diff options
141 files changed, 4468 insertions, 1593 deletions
diff --git a/Android.bp b/Android.bp index d69e51c6e46f..eae0d73f449f 100644 --- a/Android.bp +++ b/Android.bp @@ -359,6 +359,7 @@ java_defaults { "contacts-provider-platform-compat-config", ], libs: [ + "androidx.annotation_annotation", "app-compat-annotations", "ext", "framework-updatable-stubs-module_libs_api", diff --git a/cmds/idmap2/idmap2d/Idmap2Service.cpp b/cmds/idmap2/idmap2d/Idmap2Service.cpp index 1b2d905aec0a..073d987f5dad 100644 --- a/cmds/idmap2/idmap2d/Idmap2Service.cpp +++ b/cmds/idmap2/idmap2d/Idmap2Service.cpp @@ -234,7 +234,11 @@ Status Idmap2Service::createFabricatedOverlay( } for (const auto& res : overlay.entries) { - builder.SetResourceValue(res.resourceName, res.dataType, res.data); + if (res.dataType == Res_value::TYPE_STRING) { + builder.SetResourceValue(res.resourceName, res.dataType, res.stringData.value()); + } else { + builder.SetResourceValue(res.resourceName, res.dataType, res.data); + } } // Generate the file path of the fabricated overlay and ensure it does not collide with an diff --git a/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl b/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl index 6c2af274ae32..a6824da8c424 100644 --- a/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl +++ b/cmds/idmap2/idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl @@ -23,4 +23,5 @@ parcelable FabricatedOverlayInternalEntry { @utf8InCpp String resourceName; int dataType; int data; + @nullable @utf8InCpp String stringData; }
\ No newline at end of file diff --git a/cmds/idmap2/include/idmap2/FabricatedOverlay.h b/cmds/idmap2/include/idmap2/FabricatedOverlay.h index 375671881e5f..65916d7ebf49 100644 --- a/cmds/idmap2/include/idmap2/FabricatedOverlay.h +++ b/cmds/idmap2/include/idmap2/FabricatedOverlay.h @@ -41,6 +41,9 @@ struct FabricatedOverlay { Builder& SetResourceValue(const std::string& resource_name, uint8_t data_type, uint32_t data_value); + Builder& SetResourceValue(const std::string& resource_name, uint8_t data_type, + const std::string& data_string_value); + WARN_UNUSED Result<FabricatedOverlay> Build(); private: @@ -48,6 +51,7 @@ struct FabricatedOverlay { std::string resource_name; DataType data_type; DataValue data_value; + std::string data_string_value; }; std::string package_name_; diff --git a/cmds/idmap2/include/idmap2/ResourceUtils.h b/cmds/idmap2/include/idmap2/ResourceUtils.h index a0202dfee473..414aa064ada7 100644 --- a/cmds/idmap2/include/idmap2/ResourceUtils.h +++ b/cmds/idmap2/include/idmap2/ResourceUtils.h @@ -40,6 +40,7 @@ using DataValue = uint32_t; // Res_value::data struct TargetValue { DataType data_type; DataValue data_value; + std::string data_string_value; }; namespace utils { diff --git a/cmds/idmap2/libidmap2/FabricatedOverlay.cpp b/cmds/idmap2/libidmap2/FabricatedOverlay.cpp index 8352dbb7b619..4d49674efce3 100644 --- a/cmds/idmap2/libidmap2/FabricatedOverlay.cpp +++ b/cmds/idmap2/libidmap2/FabricatedOverlay.cpp @@ -65,7 +65,13 @@ FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetOverlayable(const std FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetResourceValue( const std::string& resource_name, uint8_t data_type, uint32_t data_value) { - entries_.emplace_back(Entry{resource_name, data_type, data_value}); + entries_.emplace_back(Entry{resource_name, data_type, data_value, ""}); + return *this; +} + +FabricatedOverlay::Builder& FabricatedOverlay::Builder::SetResourceValue( + const std::string& resource_name, uint8_t data_type, const std::string& data_string_value) { + entries_.emplace_back(Entry{resource_name, data_type, 0, data_string_value}); return *this; } @@ -111,7 +117,8 @@ Result<FabricatedOverlay> FabricatedOverlay::Builder::Build() { entry = type->second.insert(std::make_pair(entry_name.to_string(), TargetValue())).first; } - entry->second = TargetValue{res_entry.data_type, res_entry.data_value}; + entry->second = TargetValue{ + res_entry.data_type, res_entry.data_value, res_entry.data_string_value}; } pb::FabricatedOverlay overlay_pb; diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING index 908d3f802764..5b0bd9614f83 100644 --- a/core/java/android/app/TEST_MAPPING +++ b/core/java/android/app/TEST_MAPPING @@ -121,6 +121,9 @@ "include-annotation": "android.platform.test.annotations.Presubmit" }, { + "exclude-annotation": "android.platform.test.annotations.LargeTest" + }, + { "exclude-annotation": "androidx.test.filters.FlakyTest" }, { diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 14c671f32c21..b6b860d42ccb 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -38,15 +38,22 @@ import android.content.IntentSender; import android.content.pm.PackageManager; import android.net.MacAddress; import android.os.Handler; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.service.notification.NotificationListenerService; import android.util.ExceptionUtils; import android.util.Log; +import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.CollectionUtils; +import libcore.io.IoUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -273,6 +280,9 @@ public final class CompanionDeviceManager { @GuardedBy("mListeners") private final ArrayList<OnAssociationsChangedListenerProxy> mListeners = new ArrayList<>(); + @GuardedBy("mTransports") + private final SparseArray<Transport> mTransports = new SparseArray<>(); + /** @hide */ public CompanionDeviceManager( @Nullable ICompanionDeviceManager service, @NonNull Context context) { @@ -800,6 +810,36 @@ public final class CompanionDeviceManager { } } + /** {@hide} */ + public final void attachSystemDataTransport(int associationId, @NonNull InputStream in, + @NonNull OutputStream out) throws DeviceNotAssociatedException { + synchronized (mTransports) { + if (mTransports.contains(associationId)) { + detachSystemDataTransport(associationId); + } + + try { + final Transport transport = new Transport(associationId, in, out); + mTransports.put(associationId, transport); + transport.start(); + } catch (IOException e) { + throw new RuntimeException("Failed to attach transport", e); + } + } + } + + /** {@hide} */ + public final void detachSystemDataTransport(int associationId) + throws DeviceNotAssociatedException { + synchronized (mTransports) { + final Transport transport = mTransports.get(associationId); + if (transport != null) { + mTransports.delete(associationId); + transport.stop(); + } + } + } + /** * Associates given device with given app for the given user directly, without UI prompt. * @@ -1004,4 +1044,93 @@ public final class CompanionDeviceManager { mExecutor.execute(() -> mListener.onAssociationsChanged(associations)); } } + + /** + * Representation of an active system data transport. + * <p> + * Internally uses two threads to shuttle bidirectional data between a + * remote device and a {@code socketpair} that the system is listening to. + * This design ensures that data payloads are transported efficiently + * without adding Binder traffic contention. + */ + private class Transport { + private final int mAssociationId; + private final InputStream mRemoteIn; + private final OutputStream mRemoteOut; + + private InputStream mLocalIn; + private OutputStream mLocalOut; + + private volatile boolean mStopped; + + public Transport(int associationId, InputStream remoteIn, OutputStream remoteOut) { + mAssociationId = associationId; + mRemoteIn = remoteIn; + mRemoteOut = remoteOut; + } + + public void start() throws IOException { + final ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair(); + mLocalIn = new ParcelFileDescriptor.AutoCloseInputStream(pair[0]); + mLocalOut = new ParcelFileDescriptor.AutoCloseOutputStream(pair[0]); + + try { + mService.attachSystemDataTransport(mContext.getPackageName(), + mContext.getUserId(), mAssociationId, pair[1]); + } catch (RemoteException e) { + throw new IOException("Failed to configure transport", e); + } + + new Thread(() -> { + try { + copyWithFlushing(mLocalIn, mRemoteOut); + } catch (IOException e) { + if (!mStopped) { + Log.w(LOG_TAG, "Trouble during outgoing transport", e); + stop(); + } + } + }).start(); + new Thread(() -> { + try { + copyWithFlushing(mRemoteIn, mLocalOut); + } catch (IOException e) { + if (!mStopped) { + Log.w(LOG_TAG, "Trouble during incoming transport", e); + stop(); + } + } + }).start(); + } + + public void stop() { + mStopped = true; + + IoUtils.closeQuietly(mRemoteIn); + IoUtils.closeQuietly(mRemoteOut); + IoUtils.closeQuietly(mLocalIn); + IoUtils.closeQuietly(mLocalOut); + + try { + mService.detachSystemDataTransport(mContext.getPackageName(), + mContext.getUserId(), mAssociationId); + } catch (RemoteException e) { + Log.w(LOG_TAG, "Failed to detach transport", e); + } + } + + /** + * Copy all data from the first stream to the second stream, flushing + * after every write to ensure that we quickly deliver all pending data. + */ + private void copyWithFlushing(@NonNull InputStream in, @NonNull OutputStream out) + throws IOException { + byte[] buffer = new byte[8192]; + int c; + while ((c = in.read(buffer)) != -1) { + out.write(buffer, 0, c); + out.flush(); + } + } + } } diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java index 791fc2aaa2cb..83d2713ea114 100644 --- a/core/java/android/companion/CompanionDeviceService.java +++ b/core/java/android/companion/CompanionDeviceService.java @@ -23,11 +23,14 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.TestApi; import android.app.Service; +import android.bluetooth.BluetoothSocket; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.util.Log; +import java.io.InputStream; +import java.io.OutputStream; import java.util.Objects; import java.util.concurrent.Executor; @@ -201,6 +204,45 @@ public abstract class CompanionDeviceService extends Service { } /** + * Attach the given bidirectional communication streams to be used for + * transporting system data between associated devices. + * <p> + * The companion service providing these streams is responsible for ensuring + * that all data is transported accurately and in-order between the two + * devices, including any fragmentation and re-assembly when carried over a + * size-limited transport. + * <p> + * As an example, it's valid to provide streams obtained from a + * {@link BluetoothSocket} to this method, since {@link BluetoothSocket} + * meets the API contract described above. + * + * @param associationId id of the associated device + * @param in already connected stream of data incoming from remote + * associated device + * @param out already connected stream of data outgoing to remote associated + * device + * @hide + */ + public final void attachSystemDataTransport(int associationId, @NonNull InputStream in, + @NonNull OutputStream out) throws DeviceNotAssociatedException { + getSystemService(CompanionDeviceManager.class) + .attachSystemDataTransport(associationId, in, out); + } + + /** + * Detach any bidirectional communication streams previously configured + * through {@link #attachSystemDataTransport}. + * + * @param associationId id of the associated device + * @hide + */ + public final void detachSystemDataTransport(int associationId) + throws DeviceNotAssociatedException { + getSystemService(CompanionDeviceManager.class) + .detachSystemDataTransport(associationId); + } + + /** * Called by system whenever a device associated with this app is connected. * * @param associationInfo A record for the companion device. diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl index 085fd1b4c388..ab7dc09cf459 100644 --- a/core/java/android/companion/ICompanionDeviceManager.aidl +++ b/core/java/android/companion/ICompanionDeviceManager.aidl @@ -76,4 +76,8 @@ interface ICompanionDeviceManager { int associationId); void startSystemDataTransfer(String packageName, int userId, int associationId); + + void attachSystemDataTransport(String packageName, int userId, int associationId, in ParcelFileDescriptor fd); + + void detachSystemDataTransport(String packageName, int userId, int associationId); } diff --git a/core/java/android/content/TEST_MAPPING b/core/java/android/content/TEST_MAPPING index 5bb845d5a1a1..dac79e7124bd 100644 --- a/core/java/android/content/TEST_MAPPING +++ b/core/java/android/content/TEST_MAPPING @@ -7,6 +7,9 @@ "include-annotation": "android.platform.test.annotations.Presubmit" }, { + "exclude-annotation": "android.platform.test.annotations.LargeTest" + }, + { "exclude-annotation": "androidx.test.filters.FlakyTest" }, { diff --git a/core/java/android/content/om/FabricatedOverlay.java b/core/java/android/content/om/FabricatedOverlay.java index d62b47b34dcb..5efc1f9f7cca 100644 --- a/core/java/android/content/om/FabricatedOverlay.java +++ b/core/java/android/content/om/FabricatedOverlay.java @@ -88,7 +88,7 @@ public class FabricatedOverlay { } /** - * Sets the value of + * Sets the value of the fabricated overlay * * @param resourceName name of the target resource to overlay (in the form * [package]:type/entry) @@ -106,6 +106,25 @@ public class FabricatedOverlay { return this; } + /** + * Sets the value of the fabricated overlay + * + * @param resourceName name of the target resource to overlay (in the form + * [package]:type/entry) + * @param dataType the data type of the new value + * @param value the string representing the new value + * + * @see android.util.TypedValue#type + */ + public Builder setResourceValue(@NonNull String resourceName, int dataType, String value) { + final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry(); + entry.resourceName = resourceName; + entry.dataType = dataType; + entry.stringData = value; + mEntries.add(entry); + return this; + } + /** Builds an immutable fabricated overlay. */ public FabricatedOverlay build() { final FabricatedOverlayInternal overlay = new FabricatedOverlayInternal(); diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index b505395a091a..8bc11cbc61de 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -1448,5 +1448,15 @@ public final class DisplayManager { * @hide */ String KEY_HIGH_REFRESH_RATE_BLACKLIST = "high_refresh_rate_blacklist"; + + /** + * Key for the brightness throttling data as a String formatted: + * <displayId>,<no of throttling levels>,[<severity as string>,<brightness cap>] + * Where the latter part is repeated for each throttling level, and the entirety is repeated + * for each display, separated by a semicolon. + * For example: + * 123,1,critical,0.8;456,2,moderate,0.9,critical,0.7 + */ + String KEY_BRIGHTNESS_THROTTLING_DATA = "brightness_throttling_data"; } } diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java index f33286df7f2f..b47e92d09989 100644 --- a/core/java/android/inputmethodservice/IInputMethodWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -351,7 +351,7 @@ class IInputMethodWrapper extends IInputMethod.Stub @BinderThread @Override public void startInput(IBinder startInputToken, IRemoteInputConnection inputConnection, - EditorInfo attribute, boolean restarting, + EditorInfo editorInfo, boolean restarting, @InputMethodNavButtonFlags int navButtonFlags, @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { if (mCancellationGroup == null) { @@ -361,7 +361,7 @@ class IInputMethodWrapper extends IInputMethod.Stub final SomeArgs args = SomeArgs.obtain(); args.arg1 = startInputToken; args.arg2 = inputConnection; - args.arg3 = attribute; + args.arg3 = editorInfo; args.argi1 = restarting ? 1 : 0; args.argi2 = navButtonFlags; args.arg4 = imeDispatcher; diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 78d11bde26d3..e442e6dcd8a8 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -796,10 +796,10 @@ public class InputMethodService extends AbstractInputMethodService { */ @MainThread @Override - public void startInput(InputConnection ic, EditorInfo attribute) { - if (DEBUG) Log.v(TAG, "startInput(): editor=" + attribute); + public void startInput(InputConnection ic, EditorInfo editorInfo) { + if (DEBUG) Log.v(TAG, "startInput(): editor=" + editorInfo); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMS.startInput"); - doStartInput(ic, attribute, false); + doStartInput(ic, editorInfo, false); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } @@ -808,10 +808,10 @@ public class InputMethodService extends AbstractInputMethodService { */ @MainThread @Override - public void restartInput(InputConnection ic, EditorInfo attribute) { - if (DEBUG) Log.v(TAG, "restartInput(): editor=" + attribute); + public void restartInput(InputConnection ic, EditorInfo editorInfo) { + if (DEBUG) Log.v(TAG, "restartInput(): editor=" + editorInfo); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMS.restartInput"); - doStartInput(ic, attribute, true); + doStartInput(ic, editorInfo, true); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } @@ -2315,11 +2315,11 @@ public class InputMethodService extends AbstractInputMethodService { * setup here. You are guaranteed that {@link #onCreateInputView()} will * have been called some time before this function is called. * - * @param info Description of the type of text being edited. + * @param editorInfo Description of the type of text being edited. * @param restarting Set to true if we are restarting input on the * same text field as before. */ - public void onStartInputView(EditorInfo info, boolean restarting) { + public void onStartInputView(EditorInfo editorInfo, boolean restarting) { // Intentionally empty } @@ -2360,11 +2360,11 @@ public class InputMethodService extends AbstractInputMethodService { * editor is hidden but wants to show its candidates UI as text is * entered through some other mechanism. * - * @param info Description of the type of text being edited. + * @param editorInfo Description of the type of text being edited. * @param restarting Set to true if we are restarting input on the * same text field as before. */ - public void onStartCandidatesView(EditorInfo info, boolean restarting) { + public void onStartCandidatesView(EditorInfo editorInfo, boolean restarting) { // Intentionally empty } @@ -2902,7 +2902,7 @@ public class InputMethodService extends AbstractInputMethodService { unregisterCompatOnBackInvokedCallback(); } - void doStartInput(InputConnection ic, EditorInfo attribute, boolean restarting) { + void doStartInput(InputConnection ic, EditorInfo editorInfo, boolean restarting) { if (!restarting && mInputStarted) { doFinishInput(); } @@ -2910,13 +2910,13 @@ public class InputMethodService extends AbstractInputMethodService { null /* icProto */); mInputStarted = true; mStartedInputConnection = ic; - mInputEditorInfo = attribute; + mInputEditorInfo = editorInfo; initialize(); mInlineSuggestionSessionController.notifyOnStartInput( - attribute == null ? null : attribute.packageName, - attribute == null ? null : attribute.autofillId); + editorInfo == null ? null : editorInfo.packageName, + editorInfo == null ? null : editorInfo.autofillId); if (DEBUG) Log.v(TAG, "CALL: onStartInput"); - onStartInput(attribute, restarting); + onStartInput(editorInfo, restarting); if (mDecorViewVisible) { if (mShowInputRequested) { if (DEBUG) Log.v(TAG, "CALL: onStartInputView"); diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index eb2d64727fc1..1df7dbc0cd0b 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -24,7 +24,6 @@ import static android.graphics.Matrix.MSKEW_X; import static android.graphics.Matrix.MSKEW_Y; import static android.view.SurfaceControl.METADATA_WINDOW_TYPE; import static android.view.View.SYSTEM_UI_FLAG_VISIBLE; -import static android.view.ViewRootImpl.LOCAL_LAYOUT; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import android.animation.AnimationHandler; @@ -41,7 +40,6 @@ import android.app.Service; import android.app.WallpaperColors; import android.app.WallpaperInfo; import android.app.WallpaperManager; -import android.app.WindowConfiguration; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Intent; @@ -260,8 +258,6 @@ public abstract class WallpaperService extends Service { private final Point mLastSurfaceSize = new Point(); private final Matrix mTmpMatrix = new Matrix(); private final float[] mTmpValues = new float[9]; - private final WindowLayout mWindowLayout = new WindowLayout(); - private final Rect mTempRect = new Rect(); final WindowManager.LayoutParams mLayout = new WindowManager.LayoutParams(); @@ -1100,8 +1096,7 @@ public abstract class WallpaperService extends Service { | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; final Configuration config = mMergedConfiguration.getMergedConfiguration(); - final WindowConfiguration winConfig = config.windowConfiguration; - final Rect maxBounds = winConfig.getMaxBounds(); + final Rect maxBounds = config.windowConfiguration.getMaxBounds(); if (myWidth == ViewGroup.LayoutParams.MATCH_PARENT && myHeight == ViewGroup.LayoutParams.MATCH_PARENT) { mLayout.width = myWidth; @@ -1158,28 +1153,9 @@ public abstract class WallpaperService extends Service { } else { mLayout.surfaceInsets.set(0, 0, 0, 0); } - - int relayoutResult = 0; - if (LOCAL_LAYOUT) { - if (!mSurfaceControl.isValid()) { - relayoutResult = mSession.updateVisibility(mWindow, mLayout, - View.VISIBLE, mMergedConfiguration, mSurfaceControl, - mInsetsState, mTempControls); - } - - final Rect displayCutoutSafe = mTempRect; - mInsetsState.getDisplayCutoutSafe(displayCutoutSafe); - mWindowLayout.computeFrames(mLayout, mInsetsState, displayCutoutSafe, - winConfig.getBounds(), winConfig.getWindowingMode(), mWidth, - mHeight, mRequestedVisibilities, 1f /* compatScale */, mWinFrames); - - mSession.updateLayout(mWindow, mLayout, 0 /* flags */, mWinFrames, mWidth, - mHeight); - } else { - relayoutResult = mSession.relayout(mWindow, mLayout, mWidth, mHeight, - View.VISIBLE, 0, mWinFrames, mMergedConfiguration, - mSurfaceControl, mInsetsState, mTempControls, mSyncSeqIdBundle); - } + final int relayoutResult = mSession.relayout(mWindow, mLayout, mWidth, mHeight, + View.VISIBLE, 0, mWinFrames, mMergedConfiguration, mSurfaceControl, + mInsetsState, mTempControls, mSyncSeqIdBundle); final int transformHint = SurfaceControl.rotationToBufferTransform( (mDisplayInstallOrientation + mDisplay.getRotation()) % 4); @@ -1228,7 +1204,7 @@ public abstract class WallpaperService extends Service { null /* ignoringVisibilityState */, config.isScreenRound(), false /* alwaysConsumeSystemBars */, mLayout.softInputMode, mLayout.flags, SYSTEM_UI_FLAG_VISIBLE, mLayout.type, - winConfig.getWindowingMode(), null /* typeSideMap */); + config.windowConfiguration.getWindowingMode(), null /* typeSideMap */); if (!fixedSize) { final Rect padding = mIWallpaperEngine.mDisplayPadding; diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index bbfe18cbfa44..f1ce9c3aa04b 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -57,6 +57,17 @@ public class FeatureFlagUtils { */ public static final String SETTINGS_APP_LANGUAGE_SELECTION = "settings_app_language_selection"; + + /** + * Feature flag to allow/restrict intent redirection from/to clone profile. + * Default value is false,this is to ensure that framework is not impacted by intent redirection + * till we are ready to launch. + * From Android U onwards, this would be set to true and eventually removed. + * @hide + */ + public static final String SETTINGS_ALLOW_INTENT_REDIRECTION_FOR_CLONE_PROFILE = + "settings_allow_intent_redirection_for_clone_profile"; + /** * Support locale opt-out and opt-in switch for per app's language. * @hide @@ -119,6 +130,7 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(SETTINGS_SUPPORT_LARGE_SCREEN, "true"); DEFAULT_FLAGS.put("settings_search_always_expand", "true"); DEFAULT_FLAGS.put(SETTINGS_APP_LANGUAGE_SELECTION, "true"); + DEFAULT_FLAGS.put(SETTINGS_ALLOW_INTENT_REDIRECTION_FOR_CLONE_PROFILE, "false"); DEFAULT_FLAGS.put(SETTINGS_APP_LOCALE_OPT_IN_ENABLED, "true"); DEFAULT_FLAGS.put(SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS, "true"); DEFAULT_FLAGS.put(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME, "true"); @@ -132,6 +144,7 @@ public class FeatureFlagUtils { static { PERSISTENT_FLAGS = new HashSet<>(); PERSISTENT_FLAGS.add(SETTINGS_APP_LANGUAGE_SELECTION); + PERSISTENT_FLAGS.add(SETTINGS_ALLOW_INTENT_REDIRECTION_FOR_CLONE_PROFILE); PERSISTENT_FLAGS.add(SETTINGS_APP_LOCALE_OPT_IN_ENABLED); PERSISTENT_FLAGS.add(SETTINGS_SUPPORT_LARGE_SCREEN); PERSISTENT_FLAGS.add(SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS); diff --git a/core/java/android/view/IRemoteAnimationRunner.aidl b/core/java/android/view/IRemoteAnimationRunner.aidl index 1f64fb8ca2ec..1981c9d66c8b 100644 --- a/core/java/android/view/IRemoteAnimationRunner.aidl +++ b/core/java/android/view/IRemoteAnimationRunner.aidl @@ -46,5 +46,5 @@ oneway interface IRemoteAnimationRunner { * won't have any effect anymore. */ @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) - void onAnimationCancelled(); + void onAnimationCancelled(boolean isKeyguardOccluded); } diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 4bf68e23b79f..ef57b1ddd4b3 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -109,41 +109,6 @@ interface IWindowSession { out InsetsState insetsState, out InsetsSourceControl[] activeControls, out Bundle bundle); - /** - * Changes the view visibility and the attributes of a window. This should only be called when - * the visibility of the root view is changed. This returns a valid surface if the root view is - * visible. This also returns the latest information for the caller to compute its window frame. - * - * @param window The window being updated. - * @param attrs If non-null, new attributes to apply to the window. - * @param viewVisibility Window root view's visibility. - * @param outMergedConfiguration New config container that holds global, override and merged - * config for window, if it is now becoming visible and the merged configuration has changed - * since it was last displayed. - * @param outSurfaceControl Object in which is placed the new display surface. - * @param outInsetsState The current insets state in the system. - * @param outActiveControls The insets source controls for the caller to override the insets - * state in the system. - * - * @return int Result flags: {@link WindowManagerGlobal#RELAYOUT_FIRST_TIME}. - */ - int updateVisibility(IWindow window, in WindowManager.LayoutParams attrs, int viewVisibility, - out MergedConfiguration outMergedConfiguration, out SurfaceControl outSurfaceControl, - out InsetsState outInsetsState, out InsetsSourceControl[] outActiveControls); - - /** - * Reports the layout results and the attributes of a window to the server. - * - * @param window The window being reported. - * @param attrs If non-null, new attributes to apply to the window. - * @param flags Request flags: {@link WindowManagerGlobal#RELAYOUT_INSETS_PENDING}. - * @param clientFrames the window frames computed by the client. - * @param requestedWidth The width the window wants to be. - * @param requestedHeight The height the window wants to be. - */ - oneway void updateLayout(IWindow window, in WindowManager.LayoutParams attrs, int flags, - in ClientWindowFrames clientFrames, int requestedWidth, int requestedHeight); - /* * Notify the window manager that an application is relaunching and * windows should be prepared for replacement. diff --git a/core/java/android/view/TEST_MAPPING b/core/java/android/view/TEST_MAPPING index 50d69f78688a..ecb98f9ce801 100644 --- a/core/java/android/view/TEST_MAPPING +++ b/core/java/android/view/TEST_MAPPING @@ -10,6 +10,9 @@ "include-annotation": "android.platform.test.annotations.Presubmit" }, { + "exclude-annotation": "android.platform.test.annotations.LargeTest" + }, + { "exclude-annotation": "androidx.test.filters.FlakyTest" }, { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index beaa1a37da1d..657c0b7801b5 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -75,7 +75,6 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INSET_PARENT_ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; @@ -806,6 +805,7 @@ public final class ViewRootImpl implements ViewParent, private final ViewRootRectTracker mUnrestrictedKeepClearRectsTracker = new ViewRootRectTracker(v -> v.collectUnrestrictedPreferKeepClearRects()); private boolean mHasPendingKeepClearAreaChange; + private Rect mKeepClearAccessibilityFocusRect; private IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection; @@ -4866,13 +4866,27 @@ public final class ViewRootImpl implements ViewParent, mHandler.sendEmptyMessage(MSG_KEEP_CLEAR_RECTS_CHANGED); } - void keepClearRectsChanged() { + private void updateKeepClearForAccessibilityFocusRect() { + if (mViewConfiguration.isPreferKeepClearForFocusEnabled()) { + if (mKeepClearAccessibilityFocusRect == null) { + mKeepClearAccessibilityFocusRect = new Rect(); + } + boolean hasAccessibilityFocus = + getAccessibilityFocusedRect(mKeepClearAccessibilityFocusRect); + if (!hasAccessibilityFocus) { + mKeepClearAccessibilityFocusRect.setEmpty(); + } + mHandler.obtainMessage(MSG_KEEP_CLEAR_RECTS_CHANGED, 1, 0).sendToTarget(); + } + } + + void keepClearRectsChanged(boolean accessibilityFocusRectChanged) { boolean restrictedKeepClearRectsChanged = mKeepClearRectsTracker.computeChanges(); boolean unrestrictedKeepClearRectsChanged = mUnrestrictedKeepClearRectsTracker.computeChanges(); - if ((restrictedKeepClearRectsChanged || unrestrictedKeepClearRectsChanged) - && mView != null) { + if ((restrictedKeepClearRectsChanged || unrestrictedKeepClearRectsChanged + || accessibilityFocusRectChanged) && mView != null) { mHasPendingKeepClearAreaChange = true; // Only report keep clear areas immediately if they have not been reported recently if (!mHandler.hasMessages(MSG_REPORT_KEEP_CLEAR_RECTS)) { @@ -4889,10 +4903,16 @@ public final class ViewRootImpl implements ViewParent, } mHasPendingKeepClearAreaChange = false; - final List<Rect> restrictedKeepClearRects = mKeepClearRectsTracker.getLastComputedRects(); + List<Rect> restrictedKeepClearRects = mKeepClearRectsTracker.getLastComputedRects(); final List<Rect> unrestrictedKeepClearRects = mUnrestrictedKeepClearRectsTracker.getLastComputedRects(); + if (mKeepClearAccessibilityFocusRect != null + && !mKeepClearAccessibilityFocusRect.isEmpty()) { + restrictedKeepClearRects = new ArrayList<>(restrictedKeepClearRects); + restrictedKeepClearRects.add(mKeepClearAccessibilityFocusRect); + } + try { mWindowSession.reportKeepClearAreasChanged(mWindow, restrictedKeepClearRects, unrestrictedKeepClearRects); @@ -5092,6 +5112,7 @@ public final class ViewRootImpl implements ViewParent, // Set the new focus host and node. mAccessibilityFocusedHost = view; mAccessibilityFocusedVirtualView = node; + updateKeepClearForAccessibilityFocusRect(); if (mAttachInfo.mThreadedRenderer != null) { mAttachInfo.mThreadedRenderer.invalidateRoot(); @@ -5680,7 +5701,7 @@ public final class ViewRootImpl implements ViewParent, systemGestureExclusionChanged(); } break; case MSG_KEEP_CLEAR_RECTS_CHANGED: { - keepClearRectsChanged(); + keepClearRectsChanged(/* accessibilityFocusRectChanged= */ msg.arg1 == 1); } break; case MSG_REPORT_KEEP_CLEAR_RECTS: { reportKeepClearAreasChanged(); @@ -8043,70 +8064,20 @@ public final class ViewRootImpl implements ViewParent, final int requestedWidth = (int) (mView.getMeasuredWidth() * appScale + 0.5f); final int requestedHeight = (int) (mView.getMeasuredHeight() * appScale + 0.5f); - int relayoutResult = 0; - WindowConfiguration winConfig = getConfiguration().windowConfiguration; - if (LOCAL_LAYOUT) { - if (mFirst || viewVisibility != mViewVisibility) { - relayoutResult = mWindowSession.updateVisibility(mWindow, params, viewVisibility, - mPendingMergedConfiguration, mSurfaceControl, mTempInsets, mTempControls); - if (mTranslator != null) { - mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets); - mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls); - } - mInsetsController.onStateChanged(mTempInsets); - mInsetsController.onControlsChanged(mTempControls); - - mPendingAlwaysConsumeSystemBars = - (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0; - } - final InsetsState state = mInsetsController.getState(); - final Rect displayCutoutSafe = mTempRect; - state.getDisplayCutoutSafe(displayCutoutSafe); - if (mWindowAttributes.type == TYPE_APPLICATION_STARTING) { - // TODO(b/210378379): Remove the special logic. - // Letting starting window use the window bounds from the pending config is for the - // fixed rotation, because the config is not overridden before the starting window - // is created. - winConfig = mPendingMergedConfiguration.getMergedConfiguration() - .windowConfiguration; - } - mWindowLayout.computeFrames(mWindowAttributes, state, displayCutoutSafe, - winConfig.getBounds(), winConfig.getWindowingMode(), requestedWidth, - requestedHeight, mInsetsController.getRequestedVisibilities(), - 1f /* compatScale */, mTmpFrames); - - mWindowSession.updateLayout(mWindow, params, - insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mTmpFrames, - requestedWidth, requestedHeight); - - } else { - relayoutResult = mWindowSession.relayout(mWindow, params, - requestedWidth, requestedHeight, viewVisibility, - insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, - mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets, - mTempControls, mRelayoutBundle); - final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid"); - if (maybeSyncSeqId > 0) { - mSyncSeqId = maybeSyncSeqId; - } - - if (mTranslator != null) { - mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame); - mTranslator.translateRectInScreenToAppWindow(mTmpFrames.displayFrame); - mTranslator.translateRectInScreenToAppWindow(mTmpFrames.attachedFrame); - mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets); - mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls); - } - mInsetsController.onStateChanged(mTempInsets); - mInsetsController.onControlsChanged(mTempControls); - - mPendingAlwaysConsumeSystemBars = - (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0; + int relayoutResult = mWindowSession.relayout(mWindow, params, + requestedWidth, requestedHeight, viewVisibility, + insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, + mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets, + mTempControls, mRelayoutBundle); + final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid"); + if (maybeSyncSeqId > 0) { + mSyncSeqId = maybeSyncSeqId; } final int transformHint = SurfaceControl.rotationToBufferTransform( (mDisplayInstallOrientation + mDisplay.getRotation()) % 4); + final WindowConfiguration winConfig = getConfiguration().windowConfiguration; WindowLayout.computeSurfaceSize(mWindowAttributes, winConfig.getMaxBounds(), requestedWidth, requestedHeight, mTmpFrames.frame, mPendingDragResizing, mSurfaceSize); @@ -8155,10 +8126,23 @@ public final class ViewRootImpl implements ViewParent, destroySurface(); } + mPendingAlwaysConsumeSystemBars = + (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0; + if (restore) { params.restore(); } + + if (mTranslator != null) { + mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame); + mTranslator.translateRectInScreenToAppWindow(mTmpFrames.displayFrame); + mTranslator.translateRectInScreenToAppWindow(mTmpFrames.attachedFrame); + mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets); + mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls); + } setFrame(mTmpFrames.frame); + mInsetsController.onStateChanged(mTempInsets); + mInsetsController.onControlsChanged(mTempControls); return relayoutResult; } diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index 3a72f6e069a5..94da2741f71a 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -338,21 +338,6 @@ public class WindowlessWindowManager implements IWindowSession { } @Override - public int updateVisibility(IWindow window, WindowManager.LayoutParams inAttrs, - int viewVisibility, MergedConfiguration outMergedConfiguration, - SurfaceControl outSurfaceControl, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls) { - // TODO(b/161810301): Finish the implementation. - return 0; - } - - @Override - public void updateLayout(IWindow window, WindowManager.LayoutParams inAttrs, int flags, - ClientWindowFrames clientWindowFrames, int requestedWidth, int requestedHeight) { - // TODO(b/161810301): Finish the implementation. - } - - @Override public void prepareToReplaceWindows(android.os.IBinder appToken, boolean childrenOnly) { } diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java index 5eb24b4264e6..54ff11ccac9d 100644 --- a/core/java/android/view/inputmethod/InputMethod.java +++ b/core/java/android/view/inputmethod/InputMethod.java @@ -182,13 +182,13 @@ public interface InputMethod { * @param inputConnection Optional specific input connection for * communicating with the text box; if null, you should use the generic * bound input connection. - * @param info Information about the text box (typically, an EditText) + * @param editorInfo Information about the text box (typically, an EditText) * that requests input. * * @see EditorInfo */ @MainThread - public void startInput(InputConnection inputConnection, EditorInfo info); + public void startInput(InputConnection inputConnection, EditorInfo editorInfo); /** * This method is called when the state of this input method needs to be @@ -201,13 +201,13 @@ public interface InputMethod { * @param inputConnection Optional specific input connection for * communicating with the text box; if null, you should use the generic * bound input connection. - * @param attribute The attribute of the text box (typically, a EditText) + * @param editorInfo The attribute of the text box (typically, a EditText) * that requests input. * * @see EditorInfo */ @MainThread - public void restartInput(InputConnection inputConnection, EditorInfo attribute); + public void restartInput(InputConnection inputConnection, EditorInfo editorInfo); /** * This method is called when {@code {@link #startInput(InputConnection, EditorInfo)} or diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 6e6b48ba0b43..50f3e0cfbcf3 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -446,7 +446,7 @@ public final class InputMethodManager { * the attributes that were last retrieved from the served view and given * to the input connection. */ - EditorInfo mCurrentTextBoxAttribute; + EditorInfo mCurrentEditorInfo; /** * The InputConnection that was last retrieved from the served view. */ @@ -654,15 +654,13 @@ public final class InputMethodManager { public boolean startInput(@StartInputReason int startInputReason, View focusedView, @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode, int windowFlags) { - final View servedView; ImeTracing.getInstance().triggerClientDump( "InputMethodManager.DelegateImpl#startInput", InputMethodManager.this, null /* icProto */); synchronized (mH) { - mCurrentTextBoxAttribute = null; + mCurrentEditorInfo = null; mCompletions = null; mServedConnecting = true; - servedView = getServedViewLocked(); } return startInputInner(startInputReason, focusedView != null ? focusedView.getWindowToken() : null, startInputFlags, @@ -1600,7 +1598,7 @@ public final class InputMethodManager { checkFocus(); synchronized (mH) { - return hasServedByInputMethodLocked(view) && mCurrentTextBoxAttribute != null; + return hasServedByInputMethodLocked(view) && mCurrentEditorInfo != null; } } @@ -1610,7 +1608,7 @@ public final class InputMethodManager { public boolean isActive() { checkFocus(); synchronized (mH) { - return getServedViewLocked() != null && mCurrentTextBoxAttribute != null; + return getServedViewLocked() != null && mCurrentEditorInfo != null; } } @@ -1694,7 +1692,7 @@ public final class InputMethodManager { * to an input method */ void clearConnectionLocked() { - mCurrentTextBoxAttribute = null; + mCurrentEditorInfo = null; if (mServedInputConnection != null) { mServedInputConnection.deactivate(); mServedInputConnection = null; @@ -2192,7 +2190,7 @@ public final class InputMethodManager { public boolean doInvalidateInput(@NonNull RemoteInputConnectionImpl inputConnection, @NonNull TextSnapshot textSnapshot, int sessionId) { synchronized (mH) { - if (mServedInputConnection != inputConnection || mCurrentTextBoxAttribute == null) { + if (mServedInputConnection != inputConnection || mCurrentEditorInfo == null) { // OK to ignore because the calling InputConnection is already abandoned. return true; } @@ -2200,7 +2198,7 @@ public final class InputMethodManager { // IME is not yet bound to the client. Need to fall back to the restartInput(). return false; } - final EditorInfo editorInfo = mCurrentTextBoxAttribute.createCopyInternal(); + final EditorInfo editorInfo = mCurrentEditorInfo.createCopyInternal(); editorInfo.initialSelStart = mCursorSelStart = textSnapshot.getSelectionStart(); editorInfo.initialSelEnd = mCursorSelEnd = textSnapshot.getSelectionEnd(); mCursorCandStart = textSnapshot.getCompositionStart(); @@ -2339,7 +2337,7 @@ public final class InputMethodManager { // This is not an error. Once IME binds (MSG_BIND), InputConnection is fully // established. So we report this to interested recipients. reportInputConnectionOpened( - mServedInputConnection.getInputConnection(), mCurrentTextBoxAttribute, + mServedInputConnection.getInputConnection(), mCurrentEditorInfo, mServedInputConnectionHandler, view); } return false; @@ -2347,12 +2345,12 @@ public final class InputMethodManager { // If we already have a text box, then this view is already // connected so we want to restart it. - if (mCurrentTextBoxAttribute == null) { + if (mCurrentEditorInfo == null) { startInputFlags |= StartInputFlags.INITIAL_CONNECTION; } // Hook 'em up and let 'er rip. - mCurrentTextBoxAttribute = tba.createCopyInternal(); + mCurrentEditorInfo = tba.createCopyInternal(); mServedConnecting = false; if (mServedInputConnection != null) { @@ -2658,7 +2656,7 @@ public final class InputMethodManager { checkFocus(); synchronized (mH) { - if (!hasServedByInputMethodLocked(view) || mCurrentTextBoxAttribute == null + if (!hasServedByInputMethodLocked(view) || mCurrentEditorInfo == null || mCurrentInputMethodSession == null) { return; } @@ -2715,7 +2713,7 @@ public final class InputMethodManager { final boolean focusChanged = servedView != nextServedView; checkFocus(); synchronized (mH) { - if (!hasServedByInputMethodLocked(view) || mCurrentTextBoxAttribute == null + if (!hasServedByInputMethodLocked(view) || mCurrentEditorInfo == null || mCurrentInputMethodSession == null) { return; } @@ -2784,7 +2782,7 @@ public final class InputMethodManager { checkFocus(); synchronized (mH) { - if (!hasServedByInputMethodLocked(view) || mCurrentTextBoxAttribute == null + if (!hasServedByInputMethodLocked(view) || mCurrentEditorInfo == null || mCurrentInputMethodSession == null) { return; } @@ -2816,7 +2814,7 @@ public final class InputMethodManager { checkFocus(); synchronized (mH) { - if (!hasServedByInputMethodLocked(view) || mCurrentTextBoxAttribute == null + if (!hasServedByInputMethodLocked(view) || mCurrentEditorInfo == null || mCurrentInputMethodSession == null) { return; } @@ -2866,7 +2864,7 @@ public final class InputMethodManager { checkFocus(); synchronized (mH) { - if (!hasServedByInputMethodLocked(view) || mCurrentTextBoxAttribute == null + if (!hasServedByInputMethodLocked(view) || mCurrentEditorInfo == null || mCurrentInputMethodSession == null) { return; } @@ -3592,11 +3590,11 @@ public final class InputMethodManager { p.println(" mServedView=" + getServedViewLocked()); p.println(" mNextServedView=" + getNextServedViewLocked()); p.println(" mServedConnecting=" + mServedConnecting); - if (mCurrentTextBoxAttribute != null) { - p.println(" mCurrentTextBoxAttribute:"); - mCurrentTextBoxAttribute.dump(p, " ", false /* dumpExtras */); + if (mCurrentEditorInfo != null) { + p.println(" mCurrentEditorInfo:"); + mCurrentEditorInfo.dump(p, " ", false /* dumpExtras */); } else { - p.println(" mCurrentTextBoxAttribute: null"); + p.println(" mCurrentEditorInfo: null"); } p.println(" mServedInputConnection=" + mServedInputConnection); p.println(" mServedInputConnectionHandler=" + mServedInputConnectionHandler); @@ -3719,8 +3717,8 @@ public final class InputMethodManager { if (mCurRootView != null) { mCurRootView.dumpDebug(proto, VIEW_ROOT_IMPL); } - if (mCurrentTextBoxAttribute != null) { - mCurrentTextBoxAttribute.dumpDebug(proto, EDITOR_INFO); + if (mCurrentEditorInfo != null) { + mCurrentEditorInfo.dumpDebug(proto, EDITOR_INFO); } if (mImeInsetsConsumer != null) { mImeInsetsConsumer.dumpDebug(proto, IME_INSETS_SOURCE_CONSUMER); diff --git a/core/java/com/android/internal/inputmethod/IInputMethod.aidl b/core/java/com/android/internal/inputmethod/IInputMethod.aidl index 49eafd017687..b8196157ffd9 100644 --- a/core/java/com/android/internal/inputmethod/IInputMethod.aidl +++ b/core/java/com/android/internal/inputmethod/IInputMethod.aidl @@ -46,7 +46,7 @@ oneway interface IInputMethod { void unbindInput(); void startInput(in IBinder startInputToken, in IRemoteInputConnection inputConnection, - in EditorInfo attribute, boolean restarting, int navigationBarFlags, + in EditorInfo editorInfo, boolean restarting, int navigationBarFlags, in ImeOnBackInvokedDispatcher imeDispatcher); void onNavButtonFlagsChanged(int navButtonFlags); diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 9b19e021cc4f..508e4450d0f9 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -49,14 +49,14 @@ interface IInputMethodManager { boolean hideSoftInput(in IInputMethodClient client, IBinder windowToken, int flags, in ResultReceiver resultReceiver, int reason); // If windowToken is null, this just does startInput(). Otherwise this reports that a window - // has gained focus, and if 'attribute' is non-null then also does startInput. + // has gained focus, and if 'editorInfo' is non-null then also does startInput. // @NonNull InputBindResult startInputOrWindowGainedFocus( /* @StartInputReason */ int startInputReason, in IInputMethodClient client, in IBinder windowToken, /* @StartInputFlags */ int startInputFlags, /* @android.view.WindowManager.LayoutParams.SoftInputModeFlags */ int softInputMode, - int windowFlags, in EditorInfo attribute, in IRemoteInputConnection inputConnection, + int windowFlags, in EditorInfo editorInfo, in IRemoteInputConnection inputConnection, in IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, in ImeOnBackInvokedDispatcher imeDispatcher); diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp index 4af28ea24361..1520ea5c6831 100644 --- a/core/jni/android_graphics_BLASTBufferQueue.cpp +++ b/core/jni/android_graphics_BLASTBufferQueue.cpp @@ -41,7 +41,12 @@ struct { static JNIEnv* getenv(JavaVM* vm) { JNIEnv* env; - if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + auto result = vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6); + if (result == JNI_EDETACHED) { + if (vm->AttachCurrentThreadAsDaemon(&env, nullptr) != JNI_OK) { + LOG_ALWAYS_FATAL("Failed to AttachCurrentThread!"); + } + } else if (result != JNI_OK) { LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", vm); } return env; @@ -60,28 +65,22 @@ public: } ~TransactionHangCallbackWrapper() { - if (mTransactionHangObject) { - getenv()->DeleteGlobalRef(mTransactionHangObject); + if (mTransactionHangObject != nullptr) { + getenv(mVm)->DeleteGlobalRef(mTransactionHangObject); mTransactionHangObject = nullptr; } } void onTransactionHang(bool isGpuHang) { if (mTransactionHangObject) { - getenv()->CallVoidMethod(mTransactionHangObject, - gTransactionHangCallback.onTransactionHang, isGpuHang); + getenv(mVm)->CallVoidMethod(mTransactionHangObject, + gTransactionHangCallback.onTransactionHang, isGpuHang); } } private: JavaVM* mVm; jobject mTransactionHangObject; - - JNIEnv* getenv() { - JNIEnv* env; - mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6); - return env; - } }; static jlong nativeCreate(JNIEnv* env, jclass clazz, jstring jName, diff --git a/core/tests/companiontests/src/android/companion/CompanionTestRunner.java b/core/tests/companiontests/src/android/companion/CompanionTestRunner.java index caa2c685accc..3c59e7d716b0 100644 --- a/core/tests/companiontests/src/android/companion/CompanionTestRunner.java +++ b/core/tests/companiontests/src/android/companion/CompanionTestRunner.java @@ -33,6 +33,7 @@ public class CompanionTestRunner extends InstrumentationTestRunner { public TestSuite getAllTests() { TestSuite suite = new InstrumentationTestSuite(this); suite.addTestSuite(BluetoothDeviceFilterUtilsTest.class); + suite.addTestSuite(SystemDataTransportTest.class); return suite; } diff --git a/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java b/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java new file mode 100644 index 000000000000..be04b6c91a8a --- /dev/null +++ b/core/tests/companiontests/src/android/companion/SystemDataTransportTest.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.companion; + +import android.os.SystemClock; +import android.test.InstrumentationTestCase; +import android.util.Log; + +import com.android.internal.util.HexDump; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Random; + +public class SystemDataTransportTest extends InstrumentationTestCase { + private static final String TAG = "SystemDataTransportTest"; + + private static final int COMMAND_INVALID = 0xF00DCAFE; + private static final int COMMAND_PING_V0 = 0x50490000; + private static final int COMMAND_PONG_V0 = 0x504F0000; + + private CompanionDeviceManager mCdm; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mCdm = getInstrumentation().getTargetContext() + .getSystemService(CompanionDeviceManager.class); + } + + public void testPingHandRolled() { + // NOTE: These packets are explicitly hand-rolled to verify wire format; + // the remainder of the tests are fine using generated packets + + // PING v0 with payload "HELLO WORLD!" + final byte[] input = new byte[] { + 0x50, 0x49, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0C, + 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57, 0x4F, 0x52, 0x4C, 0x44, 0x21, + }; + // PONG v0 with payload "HELLO WORLD!" + final byte[] expected = new byte[] { + 0x50, 0x4F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0C, + 0x48, 0x45, 0x4C, 0x4C, 0x4F, 0x20, 0x57, 0x4F, 0x52, 0x4C, 0x44, 0x21, + }; + + final ByteArrayInputStream in = new ByteArrayInputStream(input); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, in, out); + + final byte[] actual = waitForByteArray(out, expected.length); + assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual)); + } + + public void testPingTrickle() { + final byte[] input = generatePacket(COMMAND_PING_V0, TAG); + final byte[] expected = generatePacket(COMMAND_PONG_V0, TAG); + + final ByteArrayInputStream in = new ByteArrayInputStream(input); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, new TrickleInputStream(in), out); + + final byte[] actual = waitForByteArray(out, expected.length); + assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual)); + } + + public void testPingDelay() { + final byte[] input = generatePacket(COMMAND_PING_V0, TAG); + final byte[] expected = generatePacket(COMMAND_PONG_V0, TAG); + + final ByteArrayInputStream in = new ByteArrayInputStream(input); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, new DelayingInputStream(in, 1000), + new DelayingOutputStream(out, 1000)); + + final byte[] actual = waitForByteArray(out, expected.length); + assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual)); + } + + public void testPingGiant() { + final byte[] blob = new byte[500_000]; + new Random().nextBytes(blob); + + final byte[] input = generatePacket(COMMAND_PING_V0, blob); + final byte[] expected = generatePacket(COMMAND_PONG_V0, blob); + + final ByteArrayInputStream in = new ByteArrayInputStream(input); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, in, out); + + final byte[] actual = waitForByteArray(out, expected.length); + assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual)); + } + + public void testMutiplePingPing() { + final byte[] input = concat(generatePacket(COMMAND_PING_V0, "red"), + generatePacket(COMMAND_PING_V0, "green")); + final byte[] expected = concat(generatePacket(COMMAND_PONG_V0, "red"), + generatePacket(COMMAND_PONG_V0, "green")); + + final ByteArrayInputStream in = new ByteArrayInputStream(input); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, in, out); + + final byte[] actual = waitForByteArray(out, expected.length); + assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual)); + } + + public void testMultipleInvalidPing() { + final byte[] input = concat(generatePacket(COMMAND_INVALID, "red"), + generatePacket(COMMAND_PING_V0, "green")); + final byte[] expected = generatePacket(COMMAND_PONG_V0, "green"); + + final ByteArrayInputStream in = new ByteArrayInputStream(input); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, in, out); + + final byte[] actual = waitForByteArray(out, expected.length); + assertEquals(HexDump.toHexString(expected), HexDump.toHexString(actual)); + } + + public void testDoubleAttach() { + // Connect an empty connection that is stalled out + final InputStream in = new EmptyInputStream(); + final OutputStream out = new ByteArrayOutputStream(); + mCdm.attachSystemDataTransport(42, in, out); + SystemClock.sleep(1000); + + // Attach a second transport that has some packets; it should disconnect + // the first transport and start replying on the second one + testPingHandRolled(); + } + + public static byte[] concat(byte[] a, byte[] b) { + return ByteBuffer.allocate(a.length + b.length).put(a).put(b).array(); + } + + public static byte[] generatePacket(int command, String data) { + return generatePacket(command, data.getBytes(StandardCharsets.UTF_8)); + } + + public static byte[] generatePacket(int command, byte[] data) { + return ByteBuffer.allocate(data.length + 8) + .putInt(command).putInt(data.length).put(data).array(); + } + + private static byte[] waitForByteArray(ByteArrayOutputStream out, int size) { + int i = 0; + while (out.size() < size) { + SystemClock.sleep(100); + if (i++ % 10 == 0) { + Log.w(TAG, "Waiting for data..."); + } + if (i > 100) { + fail(); + } + } + return out.toByteArray(); + } + + private static class EmptyInputStream extends InputStream { + @Override + public int read() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + // Instead of hanging indefinitely, wait a bit and claim that + // nothing was read, without hitting EOF + SystemClock.sleep(100); + return 0; + } + } + + private static class DelayingInputStream extends FilterInputStream { + private final long mDelay; + + public DelayingInputStream(InputStream in, long delay) { + super(in); + mDelay = delay; + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + SystemClock.sleep(mDelay); + return super.read(b, off, len); + } + } + + private static class DelayingOutputStream extends FilterOutputStream { + private final long mDelay; + + public DelayingOutputStream(OutputStream out, long delay) { + super(out); + mDelay = delay; + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + SystemClock.sleep(mDelay); + super.write(b, off, len); + } + } + + private static class TrickleInputStream extends FilterInputStream { + public TrickleInputStream(InputStream in) { + super(in); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + return super.read(b, off, 1); + } + } +} diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 1629b6ace35d..239621eeed1e 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -40,6 +40,7 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; import android.net.Uri; import android.os.Build; +import android.os.Trace; import android.system.ErrnoException; import android.system.Os; import android.util.DisplayMetrics; @@ -223,13 +224,21 @@ public final class ImageDecoder implements AutoCloseable { public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { return nCreate(mData, mOffset, mLength, preferAnimation, this); } + + @Override + public String toString() { + return "ByteArraySource{len=" + mLength + "}"; + } } private static class ByteBufferSource extends Source { ByteBufferSource(@NonNull ByteBuffer buffer) { mBuffer = buffer; + mLength = mBuffer.limit() - mBuffer.position(); } + private final ByteBuffer mBuffer; + private final int mLength; @Override public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { @@ -241,6 +250,11 @@ public final class ImageDecoder implements AutoCloseable { ByteBuffer buffer = mBuffer.slice(); return nCreate(buffer, buffer.position(), buffer.limit(), preferAnimation, this); } + + @Override + public String toString() { + return "ByteBufferSource{len=" + mLength + "}"; + } } private static class ContentResolverSource extends Source { @@ -285,6 +299,16 @@ public final class ImageDecoder implements AutoCloseable { return createFromAssetFileDescriptor(assetFd, preferAnimation, this); } + + @Override + public String toString() { + String uri = mUri.toString(); + if (uri.length() > 90) { + // We want to keep the Uri usable - usually the authority and the end is important. + uri = uri.substring(0, 80) + ".." + uri.substring(uri.length() - 10); + } + return "ContentResolverSource{uri=" + uri + "}"; + } } @NonNull @@ -399,6 +423,11 @@ public final class ImageDecoder implements AutoCloseable { return createFromStream(is, false, preferAnimation, this); } } + + @Override + public String toString() { + return "InputStream{s=" + mInputStream + "}"; + } } /** @@ -444,6 +473,11 @@ public final class ImageDecoder implements AutoCloseable { return createFromAsset(ais, preferAnimation, this); } } + + @Override + public String toString() { + return "AssetInputStream{s=" + mAssetInputStream + "}"; + } } private static class ResourceSource extends Source { @@ -485,6 +519,17 @@ public final class ImageDecoder implements AutoCloseable { return createFromAsset((AssetInputStream) is, preferAnimation, this); } + + @Override + public String toString() { + // Try to return a human-readable name for debugging purposes. + try { + return "Resource{name=" + mResources.getResourceName(mResId) + "}"; + } catch (Resources.NotFoundException e) { + // It's ok if we don't find it, fall back to ID. + } + return "Resource{id=" + mResId + "}"; + } } /** @@ -521,6 +566,11 @@ public final class ImageDecoder implements AutoCloseable { InputStream is = mAssets.open(mFileName); return createFromAsset((AssetInputStream) is, preferAnimation, this); } + + @Override + public String toString() { + return "AssetSource{file=" + mFileName + "}"; + } } private static class FileSource extends Source { @@ -534,6 +584,11 @@ public final class ImageDecoder implements AutoCloseable { public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException { return createFromFile(mFile, preferAnimation, this); } + + @Override + public String toString() { + return "FileSource{file=" + mFile + "}"; + } } private static class CallableSource extends Source { @@ -557,6 +612,11 @@ public final class ImageDecoder implements AutoCloseable { } return createFromAssetFileDescriptor(assetFd, preferAnimation, this); } + + @Override + public String toString() { + return "CallableSource{obj=" + mCallable.toString() + "}"; + } } /** @@ -1763,61 +1823,65 @@ public final class ImageDecoder implements AutoCloseable { @NonNull private static Drawable decodeDrawableImpl(@NonNull Source src, @Nullable OnHeaderDecodedListener listener) throws IOException { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeDrawable"); try (ImageDecoder decoder = src.createImageDecoder(true /*preferAnimation*/)) { decoder.mSource = src; decoder.callHeaderDecoded(listener, src); - if (decoder.mUnpremultipliedRequired) { - // Though this could be supported (ignored) for opaque images, - // it seems better to always report this error. - throw new IllegalStateException("Cannot decode a Drawable " + - "with unpremultiplied pixels!"); - } + try (ImageDecoderSourceTrace unused = new ImageDecoderSourceTrace(decoder)) { + if (decoder.mUnpremultipliedRequired) { + // Though this could be supported (ignored) for opaque images, + // it seems better to always report this error. + throw new IllegalStateException( + "Cannot decode a Drawable with unpremultiplied pixels!"); + } - if (decoder.mMutable) { - throw new IllegalStateException("Cannot decode a mutable " + - "Drawable!"); - } + if (decoder.mMutable) { + throw new IllegalStateException("Cannot decode a mutable Drawable!"); + } - // this call potentially manipulates the decoder so it must be performed prior to - // decoding the bitmap and after decode set the density on the resulting bitmap - final int srcDensity = decoder.computeDensity(src); - if (decoder.mAnimated) { - // AnimatedImageDrawable calls postProcessAndRelease only if - // mPostProcessor exists. - ImageDecoder postProcessPtr = decoder.mPostProcessor == null ? - null : decoder; - decoder.checkState(true); - Drawable d = new AnimatedImageDrawable(decoder.mNativePtr, - postProcessPtr, decoder.mDesiredWidth, - decoder.mDesiredHeight, decoder.getColorSpacePtr(), - decoder.checkForExtended(), srcDensity, - src.computeDstDensity(), decoder.mCropRect, - decoder.mInputStream, decoder.mAssetFd); - // d has taken ownership of these objects. - decoder.mInputStream = null; - decoder.mAssetFd = null; - return d; - } + // this call potentially manipulates the decoder so it must be performed prior to + // decoding the bitmap and after decode set the density on the resulting bitmap + final int srcDensity = decoder.computeDensity(src); + if (decoder.mAnimated) { + // AnimatedImageDrawable calls postProcessAndRelease only if + // mPostProcessor exists. + ImageDecoder postProcessPtr = decoder.mPostProcessor == null ? null : decoder; + decoder.checkState(true); + Drawable d = new AnimatedImageDrawable(decoder.mNativePtr, + postProcessPtr, decoder.mDesiredWidth, + decoder.mDesiredHeight, decoder.getColorSpacePtr(), + decoder.checkForExtended(), srcDensity, + src.computeDstDensity(), decoder.mCropRect, + decoder.mInputStream, decoder.mAssetFd); + // d has taken ownership of these objects. + decoder.mInputStream = null; + decoder.mAssetFd = null; + return d; + } - Bitmap bm = decoder.decodeBitmapInternal(); - bm.setDensity(srcDensity); + Bitmap bm = decoder.decodeBitmapInternal(); + bm.setDensity(srcDensity); - Resources res = src.getResources(); - byte[] np = bm.getNinePatchChunk(); - if (np != null && NinePatch.isNinePatchChunk(np)) { - Rect opticalInsets = new Rect(); - bm.getOpticalInsets(opticalInsets); - Rect padding = decoder.mOutPaddingRect; - if (padding == null) { - padding = new Rect(); + Resources res = src.getResources(); + byte[] np = bm.getNinePatchChunk(); + if (np != null && NinePatch.isNinePatchChunk(np)) { + Rect opticalInsets = new Rect(); + bm.getOpticalInsets(opticalInsets); + Rect padding = decoder.mOutPaddingRect; + if (padding == null) { + padding = new Rect(); + } + nGetPadding(decoder.mNativePtr, padding); + return new NinePatchDrawable(res, bm, np, padding, + opticalInsets, null); } - nGetPadding(decoder.mNativePtr, padding); - return new NinePatchDrawable(res, bm, np, padding, - opticalInsets, null); - } - return new BitmapDrawable(res, bm); + return new BitmapDrawable(res, bm); + } + } finally { + // Close the ImageDecoder#decode trace. + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); } } @@ -1867,26 +1931,51 @@ public final class ImageDecoder implements AutoCloseable { @NonNull private static Bitmap decodeBitmapImpl(@NonNull Source src, @Nullable OnHeaderDecodedListener listener) throws IOException { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeBitmap"); try (ImageDecoder decoder = src.createImageDecoder(false /*preferAnimation*/)) { decoder.mSource = src; decoder.callHeaderDecoded(listener, src); + try (ImageDecoderSourceTrace unused = new ImageDecoderSourceTrace(decoder)) { + // this call potentially manipulates the decoder so it must be performed prior to + // decoding the bitmap + final int srcDensity = decoder.computeDensity(src); + Bitmap bm = decoder.decodeBitmapInternal(); + bm.setDensity(srcDensity); - // this call potentially manipulates the decoder so it must be performed prior to - // decoding the bitmap - final int srcDensity = decoder.computeDensity(src); - Bitmap bm = decoder.decodeBitmapInternal(); - bm.setDensity(srcDensity); - - Rect padding = decoder.mOutPaddingRect; - if (padding != null) { - byte[] np = bm.getNinePatchChunk(); - if (np != null && NinePatch.isNinePatchChunk(np)) { - nGetPadding(decoder.mNativePtr, padding); + Rect padding = decoder.mOutPaddingRect; + if (padding != null) { + byte[] np = bm.getNinePatchChunk(); + if (np != null && NinePatch.isNinePatchChunk(np)) { + nGetPadding(decoder.mNativePtr, padding); + } } + return bm; } + } finally { + // Close the ImageDecoder#decode trace. + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + } + } - return bm; + /** + * This describes the decoder in traces to ease debugging. It has to be called after + * header has been decoded and width/height have been populated. It should be used + * inside a try-with-resources call to automatically complete the trace. + */ + private static AutoCloseable traceDecoderSource(ImageDecoder decoder) { + final boolean resourceTracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_RESOURCES); + if (resourceTracingEnabled) { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, describeDecoderForTrace(decoder)); } + + return new AutoCloseable() { + @Override + public void close() throws Exception { + if (resourceTracingEnabled) { + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + } + } + }; } // This method may modify the decoder so it must be called prior to performing the decode @@ -1994,6 +2083,66 @@ public final class ImageDecoder implements AutoCloseable { } } + /** + * Returns a short string describing what passed ImageDecoder is loading - + * it reports image dimensions, desired dimensions (if any) and source resource. + * + * The string appears in perf traces to simplify search for slow or memory intensive + * image loads. + * + * Example: ID#w=300;h=250;dw=150;dh=150;src=Resource{name=@resource} + * + * @hide + */ + private static String describeDecoderForTrace(@NonNull ImageDecoder decoder) { + StringBuilder builder = new StringBuilder(); + // Source dimensions + builder.append("ID#w="); + builder.append(decoder.mWidth); + builder.append(";h="); + builder.append(decoder.mHeight); + // Desired dimensions (if present) + if (decoder.mDesiredWidth != decoder.mWidth + || decoder.mDesiredHeight != decoder.mHeight) { + builder.append(";dw="); + builder.append(decoder.mDesiredWidth); + builder.append(";dh="); + builder.append(decoder.mDesiredHeight); + } + // Source description + builder.append(";src="); + builder.append(decoder.mSource); + return builder.toString(); + } + + /** + * Records a trace with information about the source being decoded - dimensions, + * desired dimensions and source information. + * + * It significantly eases debugging of slow resource loads on main thread and + * possible large memory consumers. + * + * @hide + */ + private static final class ImageDecoderSourceTrace implements AutoCloseable { + + private final boolean mResourceTracingEnabled; + + ImageDecoderSourceTrace(ImageDecoder decoder) { + mResourceTracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_RESOURCES); + if (mResourceTracingEnabled) { + Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, describeDecoderForTrace(decoder)); + } + } + + @Override + public void close() { + if (mResourceTracingEnabled) { + Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); + } + } + } + private static native ImageDecoder nCreate(long asset, boolean preferAnimation, Source src) throws IOException; private static native ImageDecoder nCreate(ByteBuffer buffer, int position, int limit, diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index b15dce7f3f17..41791afa45a3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -24,9 +24,9 @@ import static androidx.window.extensions.embedding.SplitContainer.getFinishSecon import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked; -import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions; +import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair; -import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; +import static androidx.window.extensions.embedding.SplitPresenter.getNonEmbeddedActivityBounds; import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; import android.app.Activity; @@ -381,6 +381,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * in a state that the caller shouldn't handle. */ @VisibleForTesting + @GuardedBy("mLock") boolean resolveActivityToContainer(@NonNull Activity activity, boolean isOnReparent) { if (isInPictureInPicture(activity) || activity.isFinishing()) { // We don't embed activity when it is in PIP, or finishing. Return true since we don't @@ -581,8 +582,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** Finds the activity below the given activity. */ + @VisibleForTesting @Nullable - private Activity findActivityBelow(@NonNull Activity activity) { + Activity findActivityBelow(@NonNull Activity activity) { Activity activityBelow = null; final TaskFragmentContainer container = getContainerWithActivity(activity); if (container != null) { @@ -606,6 +608,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Checks if there is a rule to split the two activities. If there is one, puts them into split * and returns {@code true}. Otherwise, returns {@code false}. */ + @GuardedBy("mLock") private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity); @@ -616,25 +619,25 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() - && canReuseContainer(splitRule, splitContainer.getSplitRule()) - && !boundsSmallerThanMinDimensions(primaryContainer.getLastRequestedBounds(), - getMinDimensions(primaryActivity))) { + && canReuseContainer(splitRule, splitContainer.getSplitRule())) { // Can launch in the existing secondary container if the rules share the same // presentation. final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); - if (secondaryContainer == getContainerWithActivity(secondaryActivity) - && !boundsSmallerThanMinDimensions(secondaryContainer.getLastRequestedBounds(), - getMinDimensions(secondaryActivity))) { + if (secondaryContainer == getContainerWithActivity(secondaryActivity)) { // The activity is already in the target TaskFragment. return true; } secondaryContainer.addPendingAppearedActivity(secondaryActivity); final WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.reparentActivityToTaskFragment( - secondaryContainer.getTaskFragmentToken(), - secondaryActivity.getActivityToken()); - mPresenter.applyTransaction(wct); - return true; + if (mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, + secondaryActivity, null /* secondaryIntent */) + != RESULT_EXPAND_FAILED_NO_TF_INFO) { + wct.reparentActivityToTaskFragment( + secondaryContainer.getTaskFragmentToken(), + secondaryActivity.getActivityToken()); + mPresenter.applyTransaction(wct); + return true; + } } // Create new split pair. mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule); @@ -792,6 +795,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a container for the new activity intent to launch into as splitting with the primary * activity. */ + @GuardedBy("mLock") @Nullable private TaskFragmentContainer getSecondaryContainerForSplitIfAny( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @@ -805,16 +809,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() && (canReuseContainer(splitRule, splitContainer.getSplitRule()) // TODO(b/231845476) we should always respect clearTop. - || !respectClearTop)) { - final Rect secondaryBounds = splitContainer.getSecondaryContainer() - .getLastRequestedBounds(); - if (secondaryBounds.isEmpty() - || !boundsSmallerThanMinDimensions(secondaryBounds, - getMinDimensions(intent))) { - // Can launch in the existing secondary container if the rules share the same - // presentation. - return splitContainer.getSecondaryContainer(); - } + || !respectClearTop) + && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, + null /* secondaryActivity */, intent) != RESULT_EXPAND_FAILED_NO_TF_INFO) { + // Can launch in the existing secondary container if the rules share the same + // presentation. + return splitContainer.getSecondaryContainer(); } // Create a new TaskFragment to split with the primary activity for the new activity. return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent, @@ -868,6 +868,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * if needed. * @param taskId parent Task of the new TaskFragment. */ + @GuardedBy("mLock") TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { if (activityInTask == null) { @@ -881,7 +882,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen pendingAppearedIntent, taskContainer, this); if (!taskContainer.isTaskBoundsInitialized()) { // Get the initial bounds before the TaskFragment has appeared. - final Rect taskBounds = SplitPresenter.getTaskBoundsFromActivity(activityInTask); + final Rect taskBounds = getNonEmbeddedActivityBounds(activityInTask); if (!taskContainer.setTaskBounds(taskBounds)) { Log.w(TAG, "Can't find bounds from activity=" + activityInTask); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 1b79ad999435..a89847a30d20 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -65,6 +65,41 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface Position {} + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)}. + * No need to expand the splitContainer because screen is big enough to + * {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} and minimum dimensions is satisfied. + */ + static final int RESULT_NOT_EXPANDED = 0; + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)}. + * The splitContainer should be expanded. It is usually because minimum dimensions is not + * satisfied. + * @see #shouldShowSideBySide(Rect, SplitRule, Pair) + */ + static final int RESULT_EXPANDED = 1; + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)}. + * The splitContainer should be expanded, but the client side hasn't received + * {@link android.window.TaskFragmentInfo} yet. Fallback to create new expanded SplitContainer + * instead. + */ + static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2; + + /** + * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, + * Activity, Activity, Intent)} + */ + @IntDef(value = { + RESULT_NOT_EXPANDED, + RESULT_EXPANDED, + RESULT_EXPAND_FAILED_NO_TF_INFO, + }) + private @interface ResultCode {} + private final SplitController mController; SplitPresenter(@NonNull Executor executor, SplitController controller) { @@ -396,6 +431,44 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { super.updateWindowingMode(wct, fragmentToken, windowingMode); } + /** + * Expands the split container if the current split bounds are smaller than the Activity or + * Intent that is added to the container. + * + * @return the {@link ResultCode} based on {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} + * and if {@link android.window.TaskFragmentInfo} has reported to the client side. + */ + @ResultCode + int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct, + @NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity, + @Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent) { + if (secondaryActivity == null && secondaryIntent == null) { + throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be" + + " non-null."); + } + final Rect taskBounds = getParentContainerBounds(primaryActivity); + final Pair<Size, Size> minDimensionsPair; + if (secondaryActivity != null) { + minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); + } else { + minDimensionsPair = getActivityIntentMinDimensionsPair(primaryActivity, + secondaryIntent); + } + // Expand the splitContainer if minimum dimensions are not satisfied. + if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) { + // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment + // bounds. Return failure to create a new SplitContainer which fills task bounds. + if (splitContainer.getPrimaryContainer().getInfo() == null + || splitContainer.getSecondaryContainer().getInfo() == null) { + return RESULT_EXPAND_FAILED_NO_TF_INFO; + } + expandTaskFragment(wct, splitContainer.getPrimaryContainer().getTaskFragmentToken()); + expandTaskFragment(wct, splitContainer.getSecondaryContainer().getTaskFragmentToken()); + return RESULT_EXPANDED; + } + return RESULT_NOT_EXPANDED; + } + static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) { return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */); } @@ -565,11 +638,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { if (container != null) { return getParentContainerBounds(container); } - return getTaskBoundsFromActivity(activity); + // Obtain bounds from Activity instead because the Activity hasn't been embedded yet. + return getNonEmbeddedActivityBounds(activity); } + /** + * Obtains the bounds from a non-embedded Activity. + * <p> + * Note that callers should use {@link #getParentContainerBounds(Activity)} instead for most + * cases unless we want to obtain task bounds before + * {@link TaskContainer#isTaskBoundsInitialized()}. + */ @NonNull - static Rect getTaskBoundsFromActivity(@NonNull Activity activity) { + static Rect getNonEmbeddedActivityBounds(@NonNull Activity activity) { final WindowConfiguration windowConfiguration = activity.getResources().getConfiguration().windowConfiguration; if (!activity.isInMultiWindowMode()) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java index 1ac33173668b..c4f37091a491 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java @@ -83,9 +83,9 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { if (TaskFragmentAnimationController.DEBUG) { - Log.v(TAG, "onAnimationCancelled"); + Log.v(TAG, "onAnimationCancelled: isKeyguardOccluded=" + isKeyguardOccluded); } mHandler.post(this::cancelAnimation); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java index 835c40365cda..effc1a3ef3ea 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.mock; import android.annotation.NonNull; import android.app.Activity; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; @@ -57,13 +58,21 @@ public class EmbeddingTestUtils { /** Creates a rule to always split the given activity and the given intent. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent) { + return createSplitRule(primaryActivity, secondaryIntent, true /* clearTop */); + } + + /** Creates a rule to always split the given activity and the given intent. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent, boolean clearTop) { final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent); return new SplitPairRule.Builder( activityPair -> false, targetPair::equals, w -> true) .setSplitRatio(SPLIT_RATIO) - .setShouldClearTop(true) + .setShouldClearTop(clearTop) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) .build(); } @@ -75,6 +84,14 @@ public class EmbeddingTestUtils { true /* clearTop */); } + /** Creates a rule to always split the given activities. */ + static SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean clearTop) { + return createSplitRule(primaryActivity, secondaryActivity, + DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY, + clearTop); + } + /** Creates a rule to always split the given activities with the given finish behaviors. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary, @@ -105,4 +122,12 @@ public class EmbeddingTestUtils { false /* isTaskFragmentClearedForPip */, new Point()); } + + static ActivityInfo createActivityInfoWithMinDimensions() { + ActivityInfo aInfo = new ActivityInfo(); + final Rect primaryBounds = getSplitBounds(true /* isPrimary */); + aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, + primaryBounds.width() + 1, primaryBounds.height() + 1); + return aInfo; + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index ef7728cec387..042547fd30f2 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; @@ -34,6 +35,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; @@ -436,6 +438,50 @@ public class SplitControllerTest { } @Test + public void testResolveStartActivityIntent_shouldExpandSplitContainer() { + final Intent intent = new Intent().setComponent( + new ComponentName(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class)); + setupSplitRule(mActivity, intent, false /* clearTop */); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); + assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); + assertTrue(container.areLastRequestedBoundsEqual(null)); + assertEquals(container, mSplitController.getContainerWithActivity(secondaryActivity)); + } + + @Test + public void testResolveStartActivityIntent_noInfo_shouldCreateSplitContainer() { + final Intent intent = new Intent().setComponent( + new ComponentName(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class)); + setupSplitRule(mActivity, intent, false /* clearTop */); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); + + final TaskFragmentContainer secondaryContainer = mSplitController + .getContainerWithActivity(secondaryActivity); + secondaryContainer.mInfo = null; + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); + assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); + assertTrue(container.areLastRequestedBoundsEqual(null)); + assertNotEquals(container, secondaryContainer); + } + + @Test public void testPlaceActivityInTopContainer() { mSplitController.placeActivityInTopContainer(mActivity); @@ -787,11 +833,7 @@ public class SplitControllerTest { final Activity activityBelow = createMockActivity(); setupSplitRule(mActivity, activityBelow); - ActivityInfo aInfo = new ActivityInfo(); - final Rect primaryBounds = getSplitBounds(true /* isPrimary */); - aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, - primaryBounds.width() + 1, primaryBounds.height() + 1); - doReturn(aInfo).when(mActivity).getActivityInfo(); + doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, TASK_ID); @@ -810,17 +852,12 @@ public class SplitControllerTest { final Activity activityBelow = createMockActivity(); setupSplitRule(activityBelow, mActivity); - ActivityInfo aInfo = new ActivityInfo(); - final Rect secondaryBounds = getSplitBounds(false /* isPrimary */); - aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, - secondaryBounds.width() + 1, secondaryBounds.height() + 1); - doReturn(aInfo).when(mActivity).getActivityInfo(); + doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, TASK_ID); container.addPendingAppearedActivity(mActivity); - // Allow to split as primary. boolean result = mSplitController.resolveActivityToContainer(mActivity, false /* isOnReparent */); @@ -828,6 +865,29 @@ public class SplitControllerTest { assertSplitPair(activityBelow, mActivity, true /* matchParentBounds */); } + // Suppress GuardedBy warning on unit tests + @SuppressWarnings("GuardedBy") + @Test + public void testResolveActivityToContainer_minDimensions_shouldExpandSplitContainer() { + final Activity primaryActivity = createMockActivity(); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, secondaryActivity, false /* clearTop */); + + setupSplitRule(primaryActivity, mActivity, false /* clearTop */); + doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); + doReturn(secondaryActivity).when(mSplitController).findActivityBelow(eq(mActivity)); + + clearInvocations(mSplitPresenter); + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* isOnReparent */); + + assertTrue(result); + assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */); + assertEquals(mSplitController.getContainerWithActivity(secondaryActivity), + mSplitController.getContainerWithActivity(mActivity)); + verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any()); + } + @Test public void testResolveActivityToContainer_inUnknownTaskFragment() { doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity); @@ -944,23 +1004,41 @@ public class SplitControllerTest { /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent) { - final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent); + setupSplitRule(primaryActivity, secondaryIntent, true /* clearTop */); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent, boolean clearTop) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent, clearTop); mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); } /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { - final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity); + setupSplitRule(primaryActivity, secondaryActivity, true /* clearTop */); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean clearTop) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity, clearTop); mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); } /** Adds a pair of TaskFragments as split for the given activities. */ private void addSplitTaskFragments(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { + addSplitTaskFragments(primaryActivity, secondaryActivity, true /* clearTop */); + } + + /** Adds a pair of TaskFragments as split for the given activities. */ + private void addSplitTaskFragments(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity, boolean clearTop) { registerSplitPair(createMockTaskFragmentContainer(primaryActivity), createMockTaskFragmentContainer(secondaryActivity), - createSplitRule(primaryActivity, secondaryActivity)); + createSplitRule(primaryActivity, secondaryActivity, clearTop)); } /** Registers the two given TaskFragments as split pair. */ @@ -1011,16 +1089,18 @@ public class SplitControllerTest { if (primaryContainer.mInfo != null) { final Rect primaryBounds = matchParentBounds ? new Rect() : getSplitBounds(true /* isPrimary */); + final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED + : WINDOWING_MODE_MULTI_WINDOW; assertTrue(primaryContainer.areLastRequestedBoundsEqual(primaryBounds)); - assertTrue(primaryContainer.isLastRequestedWindowingModeEqual( - WINDOWING_MODE_MULTI_WINDOW)); + assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(windowingMode)); } if (secondaryContainer.mInfo != null) { final Rect secondaryBounds = matchParentBounds ? new Rect() : getSplitBounds(false /* isPrimary */); + final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED + : WINDOWING_MODE_MULTI_WINDOW; assertTrue(secondaryContainer.areLastRequestedBoundsEqual(secondaryBounds)); - assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual( - WINDOWING_MODE_MULTI_WINDOW)); + assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual(windowingMode)); } } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index acc398a27baf..d79319666c01 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -20,11 +20,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_END; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_FILL; import static androidx.window.extensions.embedding.SplitPresenter.POSITION_START; +import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPANDED; +import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; +import static androidx.window.extensions.embedding.SplitPresenter.RESULT_NOT_EXPANDED; import static androidx.window.extensions.embedding.SplitPresenter.getBoundsForPosition; import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; @@ -34,6 +39,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -49,6 +55,7 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.util.Pair; import android.util.Size; @@ -195,6 +202,52 @@ public class SplitPresenterTest { splitRule, mActivity, minDimensionsPair)); } + @Test + public void testExpandSplitContainerIfNeeded() { + SplitContainer splitContainer = mock(SplitContainer.class); + Activity secondaryActivity = createMockActivity(); + SplitRule splitRule = createSplitRule(mActivity, secondaryActivity); + TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID); + TaskFragmentContainer secondaryTf = mController.newContainer(secondaryActivity, TASK_ID); + doReturn(splitRule).when(splitContainer).getSplitRule(); + doReturn(primaryTf).when(splitContainer).getPrimaryContainer(); + doReturn(secondaryTf).when(splitContainer).getSecondaryContainer(); + + assertThrows(IllegalArgumentException.class, () -> + mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, + null /* secondaryActivity */, null /* secondaryIntent */)); + + assertEquals(RESULT_NOT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, + splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); + verify(mPresenter, never()).expandTaskFragment(any(), any()); + + doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo(); + assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded( + mTransaction, splitContainer, mActivity, secondaryActivity, + null /* secondaryIntent */)); + + primaryTf.setInfo(createMockTaskFragmentInfo(primaryTf, mActivity)); + secondaryTf.setInfo(createMockTaskFragmentInfo(secondaryTf, secondaryActivity)); + + assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, + splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); + verify(mPresenter).expandTaskFragment(eq(mTransaction), + eq(primaryTf.getTaskFragmentToken())); + verify(mPresenter).expandTaskFragment(eq(mTransaction), + eq(secondaryTf.getTaskFragmentToken())); + + clearInvocations(mPresenter); + + assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, + splitContainer, mActivity, null /* secondaryActivity */, + new Intent(ApplicationProvider.getApplicationContext(), + MinimumDimensionActivity.class))); + verify(mPresenter).expandTaskFragment(eq(mTransaction), + eq(primaryTf.getTaskFragmentToken())); + verify(mPresenter).expandTaskFragment(eq(mTransaction), + eq(secondaryTf.getTaskFragmentToken())); + } + private Activity createMockActivity() { final Activity activity = mock(Activity.class); final Configuration activityConfig = new Configuration(); @@ -203,6 +256,7 @@ public class SplitPresenterTest { doReturn(mActivityResources).when(activity).getResources(); doReturn(activityConfig).when(mActivityResources).getConfiguration(); doReturn(new ActivityInfo()).when(activity).getActivityInfo(); + doReturn(mock(IBinder.class)).when(activity).getActivityToken(); return activity; } } diff --git a/libs/WindowManager/Shell/res/values-television/config.xml b/libs/WindowManager/Shell/res/values-television/config.xml index 86ca65526336..cc0333efd82b 100644 --- a/libs/WindowManager/Shell/res/values-television/config.xml +++ b/libs/WindowManager/Shell/res/values-television/config.xml @@ -43,4 +43,13 @@ <!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself if a custom action is present before closing it. --> <integer name="config_pipForceCloseDelay">5000</integer> + + <!-- Animation duration when exit starting window: fade out icon --> + <integer name="starting_window_app_reveal_icon_fade_out_duration">0</integer> + + <!-- Animation duration when exit starting window: reveal app --> + <integer name="starting_window_app_reveal_anim_delay">0</integer> + + <!-- Animation duration when exit starting window: reveal app --> + <integer name="starting_window_app_reveal_anim_duration">0</integer> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index b2f09895d7d8..68a08513e7f5 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -157,7 +157,7 @@ <string name="accessibility_bubble_dismissed">Bubble dismissed.</string> <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] --> - <string name="restart_button_description">Tap to restart this app and go full screen.</string> + <string name="restart_button_description">Tap to restart this app for a better view.</string> <!-- Description of the camera compat button for applying stretched issues treatment in the hint for compatibility control. [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index bd386b5681d8..37c0c9b01c88 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -942,7 +942,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // Re-set the PIP bounds to none. mPipBoundsState.setBounds(new Rect()); mPipUiEventLoggerLogger.setTaskInfo(null); - mPipMenuController.detach(); + mMainExecutor.executeDelayed(() -> mPipMenuController.detach(), 0); if (info.displayId != Display.DEFAULT_DISPLAY && mOnDisplayIdChangeCallback != null) { mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index c80c14f353d0..05a890fc65ed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -715,7 +715,7 @@ public class PipTransition extends PipTransitionController { mSurfaceTransactionHelper .crop(finishTransaction, leash, destinationBounds) .round(finishTransaction, leash, true /* applyCornerRadius */); - mPipMenuController.attach(leash); + mTransitions.getMainExecutor().executeDelayed(() -> mPipMenuController.attach(leash), 0); if (taskInfo.pictureInPictureParams != null && taskInfo.pictureInPictureParams.isAutoEnterEnabled() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index a43b6043908b..90a2695bdf90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -28,9 +28,7 @@ import android.app.TaskInfo; import android.content.ComponentName; import android.content.pm.ActivityInfo; import android.graphics.Rect; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -56,7 +54,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected final ShellTaskOrganizer mShellTaskOrganizer; protected final PipMenuController mPipMenuController; protected final Transitions mTransitions; - private final Handler mMainHandler; private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); protected PipTaskOrganizer mPipOrganizer; @@ -144,7 +141,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipAnimationController = pipAnimationController; mTransitions = transitions; - mMainHandler = new Handler(Looper.getMainLooper()); if (Transitions.ENABLE_SHELL_TRANSITIONS) { transitions.addHandler(this); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 4e7b20e3ae84..59b0afe22acb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -457,10 +457,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { onRemoteAnimationFinishedOrCancelled(evictWct); try { - adapter.getRunner().onAnimationCancelled(); + adapter.getRunner().onAnimationCancelled(isKeyguardOccluded); } catch (RemoteException e) { Slog.e(TAG, "Error starting remote animation", e); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/OWNERS new file mode 100644 index 000000000000..d325d161ac53 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-module TV splitscreen owner +galinap@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index 0d309c1cc21a..19d3acbf28d4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -20,10 +20,8 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.graphics.Color.WHITE; import static android.graphics.Color.alpha; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import static android.view.ViewRootImpl.LOCAL_LAYOUT; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; -import static android.view.WindowLayout.UNSPECIFIED_LENGTH; import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; import static android.view.WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; @@ -53,7 +51,6 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManager.TaskDescription; import android.app.ActivityThread; -import android.app.WindowConfiguration; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; @@ -80,7 +77,6 @@ import android.view.SurfaceSession; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; -import android.view.WindowLayout; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.window.ClientWindowFrames; @@ -212,8 +208,6 @@ public class TaskSnapshotWindow { final IWindowSession session = WindowManagerGlobal.getWindowSession(); final SurfaceControl surfaceControl = new SurfaceControl(); final ClientWindowFrames tmpFrames = new ClientWindowFrames(); - final WindowLayout windowLayout = new WindowLayout(); - final Rect displayCutoutSafe = new Rect(); final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0]; final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration(); @@ -251,25 +245,9 @@ public class TaskSnapshotWindow { window.setOuter(snapshotSurface); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout"); - if (LOCAL_LAYOUT) { - if (!surfaceControl.isValid()) { - session.updateVisibility(window, layoutParams, View.VISIBLE, - tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls); - } - tmpInsetsState.getDisplayCutoutSafe(displayCutoutSafe); - final WindowConfiguration winConfig = - tmpMergedConfiguration.getMergedConfiguration().windowConfiguration; - windowLayout.computeFrames(layoutParams, tmpInsetsState, displayCutoutSafe, - winConfig.getBounds(), winConfig.getWindowingMode(), UNSPECIFIED_LENGTH, - UNSPECIFIED_LENGTH, info.requestedVisibilities, 1f /* compatScale */, - tmpFrames); - session.updateLayout(window, layoutParams, 0 /* flags */, tmpFrames, - UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH); - } else { - session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, - tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, - tmpControls, new Bundle()); - } + session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, + tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, + tmpControls, new Bundle()); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java index 61e11e877b90..61e92f355dc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java @@ -107,7 +107,7 @@ public class LegacyTransitions { } @Override - public void onAnimationCancelled() throws RemoteException { + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { mCancelled = true; mApps = mWallpapers = mNonApps = null; checkApply(); diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp index cab3f8414d7b..130b204954b4 100644 --- a/libs/input/SpriteController.cpp +++ b/libs/input/SpriteController.cpp @@ -131,8 +131,9 @@ void SpriteController::doUpdateSprites() { update.state.surfaceHeight = update.state.icon.height(); update.state.surfaceDrawn = false; update.state.surfaceVisible = false; - update.state.surfaceControl = obtainSurface( - update.state.surfaceWidth, update.state.surfaceHeight); + update.state.surfaceControl = + obtainSurface(update.state.surfaceWidth, update.state.surfaceHeight, + update.state.displayId); if (update.state.surfaceControl != NULL) { update.surfaceChanged = surfaceChanged = true; } @@ -168,8 +169,8 @@ void SpriteController::doUpdateSprites() { } } - // If surface is a new one, we have to set right layer stack. - if (update.surfaceChanged || update.state.dirty & DIRTY_DISPLAY_ID) { + // If surface has changed to a new display, we have to reparent it. + if (update.state.dirty & DIRTY_DISPLAY_ID) { t.reparent(update.state.surfaceControl, mParentSurfaceProvider(update.state.displayId)); needApplyTransaction = true; } @@ -330,21 +331,28 @@ void SpriteController::ensureSurfaceComposerClient() { } } -sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height) { +sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height, + int32_t displayId) { ensureSurfaceComposerClient(); - sp<SurfaceControl> surfaceControl = mSurfaceComposerClient->createSurface( - String8("Sprite"), width, height, PIXEL_FORMAT_RGBA_8888, - ISurfaceComposerClient::eHidden | - ISurfaceComposerClient::eCursorWindow); - if (surfaceControl == NULL || !surfaceControl->isValid()) { + const sp<SurfaceControl> parent = mParentSurfaceProvider(displayId); + if (parent == nullptr) { + ALOGE("Failed to get the parent surface for pointers on display %d", displayId); + } + + const sp<SurfaceControl> surfaceControl = + mSurfaceComposerClient->createSurface(String8("Sprite"), width, height, + PIXEL_FORMAT_RGBA_8888, + ISurfaceComposerClient::eHidden | + ISurfaceComposerClient::eCursorWindow, + parent ? parent->getHandle() : nullptr); + if (surfaceControl == nullptr || !surfaceControl->isValid()) { ALOGE("Error creating sprite surface."); - return NULL; + return nullptr; } return surfaceControl; } - // --- SpriteController::SpriteImpl --- SpriteController::SpriteImpl::SpriteImpl(const sp<SpriteController> controller) : diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h index 2e9cb9685c46..1f113c045360 100644 --- a/libs/input/SpriteController.h +++ b/libs/input/SpriteController.h @@ -265,7 +265,7 @@ private: void doDisposeSurfaces(); void ensureSurfaceComposerClient(); - sp<SurfaceControl> obtainSurface(int32_t width, int32_t height); + sp<SurfaceControl> obtainSurface(int32_t width, int32_t height, int32_t displayId); }; } // namespace android diff --git a/libs/protoutil/Android.bp b/libs/protoutil/Android.bp index 132d71eb6db8..128be3c33ac5 100644 --- a/libs/protoutil/Android.bp +++ b/libs/protoutil/Android.bp @@ -55,6 +55,7 @@ cc_library { export_include_dirs: ["include"], + min_sdk_version: "30", apex_available: [ "//apex_available:platform", "com.android.os.statsd", @@ -81,5 +82,5 @@ cc_test { proto: { type: "full", - } + }, } diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index 6b9daa35f9ca..cf23a5446f91 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -48,7 +48,6 @@ import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.text.format.Formatter; -import android.util.IconDrawableFactory; import android.util.Log; import android.util.SparseArray; @@ -116,7 +115,6 @@ public class ApplicationsState { final Context mContext; final PackageManager mPm; - final IconDrawableFactory mDrawableFactory; final IPackageManager mIpm; final UserManager mUm; final StorageStatsManager mStats; @@ -194,7 +192,6 @@ public class ApplicationsState { private ApplicationsState(Application app, IPackageManager iPackageManager) { mContext = app; mPm = mContext.getPackageManager(); - mDrawableFactory = IconDrawableFactory.newInstance(mContext); mIpm = iPackageManager; mUm = mContext.getSystemService(UserManager.class); mStats = mContext.getSystemService(StorageStatsManager.class); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index 89e10c4b5e11..fc70ba40fb47 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -20,15 +20,19 @@ import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; +import android.os.Build; import android.os.ParcelUuid; import android.util.Log; +import androidx.annotation.ChecksSdkIntAtLeast; + import com.android.internal.annotations.VisibleForTesting; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * CsipDeviceManager manages the set of remote CSIP Bluetooth devices. @@ -126,32 +130,84 @@ public class CsipDeviceManager { } } + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) + private static boolean isAtLeastT() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; + } + // Group devices by groupId @VisibleForTesting void onGroupIdChanged(int groupId) { - int firstMatchedIndex = -1; - CachedBluetoothDevice mainDevice = null; + if (!isValidGroupId(groupId)) { + log("onGroupIdChanged: groupId is invalid"); + return; + } + log("onGroupIdChanged: mCachedDevices list =" + mCachedDevices.toString()); + final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); + final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager(); + final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile(); + final BluetoothDevice mainBluetoothDevice = (leAudioProfile != null && isAtLeastT()) ? + leAudioProfile.getConnectedGroupLeadDevice(groupId) : null; + CachedBluetoothDevice newMainDevice = + mainBluetoothDevice != null ? deviceManager.findDevice(mainBluetoothDevice) : null; + if (newMainDevice != null) { + final CachedBluetoothDevice finalNewMainDevice = newMainDevice; + final List<CachedBluetoothDevice> memberDevices = mCachedDevices.stream() + .filter(cachedDevice -> !cachedDevice.equals(finalNewMainDevice) + && cachedDevice.getGroupId() == groupId) + .collect(Collectors.toList()); + if (memberDevices == null || memberDevices.isEmpty()) { + log("onGroupIdChanged: There is no member device in list."); + return; + } + log("onGroupIdChanged: removed from UI device =" + memberDevices + + ", with groupId=" + groupId + " mainDevice= " + newMainDevice); + for (CachedBluetoothDevice memberDeviceItem : memberDevices) { + Set<CachedBluetoothDevice> memberSet = memberDeviceItem.getMemberDevice(); + if (!memberSet.isEmpty()) { + log("onGroupIdChanged: Transfer the member list into new main device."); + for (CachedBluetoothDevice memberListItem : memberSet) { + if (!memberListItem.equals(newMainDevice)) { + newMainDevice.addMemberDevice(memberListItem); + } + } + memberSet.clear(); + } - for (int i = mCachedDevices.size() - 1; i >= 0; i--) { - final CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); - if (cachedDevice.getGroupId() != groupId) { - continue; + newMainDevice.addMemberDevice(memberDeviceItem); + mCachedDevices.remove(memberDeviceItem); + mBtManager.getEventManager().dispatchDeviceRemoved(memberDeviceItem); } - if (firstMatchedIndex == -1) { - // Found the first one - firstMatchedIndex = i; - mainDevice = cachedDevice; - continue; + if (!mCachedDevices.contains(newMainDevice)) { + mCachedDevices.add(newMainDevice); + mBtManager.getEventManager().dispatchDeviceAdded(newMainDevice); } + } else { + log("onGroupIdChanged: There is no main device from the LE profile."); + int firstMatchedIndex = -1; - log("onGroupIdChanged: removed from UI device =" + cachedDevice - + ", with groupId=" + groupId + " firstMatchedIndex=" + firstMatchedIndex); + for (int i = mCachedDevices.size() - 1; i >= 0; i--) { + final CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); + if (cachedDevice.getGroupId() != groupId) { + continue; + } + + if (firstMatchedIndex == -1) { + // Found the first one + firstMatchedIndex = i; + newMainDevice = cachedDevice; + continue; + } + + log("onGroupIdChanged: removed from UI device =" + cachedDevice + + ", with groupId=" + groupId + " firstMatchedIndex=" + firstMatchedIndex); - mainDevice.addMemberDevice(cachedDevice); - mCachedDevices.remove(i); - mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice); - break; + newMainDevice.addMemberDevice(cachedDevice); + mCachedDevices.remove(i); + mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice); + break; + } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java index 19df1e9c0730..0f57d8785de9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java @@ -21,6 +21,7 @@ import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; +import android.annotation.Nullable; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothCodecConfig; @@ -183,6 +184,37 @@ public class LeAudioProfile implements LocalBluetoothProfile { return mBluetoothAdapter.getActiveDevices(BluetoothProfile.LE_AUDIO); } + /** + * Get Lead device for the group. + * + * Lead device is the device that can be used as an active device in the system. + * Active devices points to the Audio Device for the Le Audio group. + * This method returns the Lead devices for the connected LE Audio + * group and this device should be used in the setActiveDevice() method by other parts + * of the system, which wants to set to active a particular Le Audio group. + * + * Note: getActiveDevice() returns the Lead device for the currently active LE Audio group. + * Note: When Lead device gets disconnected while Le Audio group is active and has more devices + * in the group, then Lead device will not change. If Lead device gets disconnected, for the + * Le Audio group which is not active, a new Lead device will be chosen + * + * @param groupId The group id. + * @return group lead device. + * + * @hide + */ + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + public @Nullable BluetoothDevice getConnectedGroupLeadDevice(int groupId) { + if (DEBUG) { + Log.d(TAG,"getConnectedGroupLeadDevice"); + } + if (mService == null) { + Log.e(TAG,"No service."); + return null; + } + return mService.getConnectedGroupLeadDevice(groupId); + } + @Override public boolean isEnabled(BluetoothDevice device) { if (mService == null || device == null) { diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index 5b47ae525919..fbe33565f11d 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -656,7 +656,7 @@ class ActivityLaunchAnimator( controller.onLaunchAnimationCancelled() } - override fun onAnimationCancelled() { + override fun onAnimationCancelled(isKeyguardOccluded: Boolean) { if (timedOut) { return } diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index 77f1803523a8..acf3e4dcf02a 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -102,12 +102,13 @@ screen. --> <item name="half_opened_bouncer_height_ratio" type="dimen" format="float">0.0</item> - <!-- The actual amount of translation that is applied to the bouncer when it animates from one - side of the screen to the other in one-handed mode. Note that it will always translate from - the side of the screen to the other (it will "jump" closer to the destination while the - opacity is zero), but this controls how much motion will actually be applied to it while - animating. Larger values will cause it to move "faster" while fading out/in. --> - <dimen name="one_handed_bouncer_move_animation_translation">120dp</dimen> + <!-- The actual amount of translation that is applied to the security when it animates from one + side of the screen to the other in one-handed or user switcher mode. Note that it will + always translate from the side of the screen to the other (it will "jump" closer to the + destination while the opacity is zero), but this controls how much motion will actually be + applied to it while animating. Larger values will cause it to move "faster" while + fading out/in. --> + <dimen name="security_shift_animation_translation">120dp</dimen> <dimen name="bouncer_user_switcher_header_text_size">20sp</dimen> diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml index 3f56bafef134..efbdd1af3644 100644 --- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml +++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml @@ -20,5 +20,5 @@ style="@style/clock_subtitle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:format12Hour="EEE, MMM d" - android:format24Hour="EEE, MMM d"/> + android:format12Hour="@string/dream_date_complication_date_format" + android:format24Hour="@string/dream_date_complication_date_format"/> diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml index 27ee428d3528..5f4e310f975c 100644 --- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml +++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml @@ -22,8 +22,8 @@ android:fontFamily="@*android:string/config_clockFontFamily" android:includeFontPadding="false" android:textColor="@android:color/white" - android:format12Hour="h:mm" - android:format24Hour="kk:mm" + android:format12Hour="@string/dream_time_complication_12_hr_time_format" + android:format24Hour="@string/dream_time_complication_24_hr_time_format" android:shadowColor="@color/keyguard_shadow_color" android:shadowRadius="?attr/shadowRadius" android:textSize="@dimen/dream_overlay_complication_clock_time_text_size"/> diff --git a/packages/SystemUI/res/values-h800dp/dimens.xml b/packages/SystemUI/res/values-h800dp/dimens.xml index e6af6f46ae69..94fe20955ce6 100644 --- a/packages/SystemUI/res/values-h800dp/dimens.xml +++ b/packages/SystemUI/res/values-h800dp/dimens.xml @@ -15,9 +15,6 @@ --> <resources> - <!-- Minimum margin between clock and top of screen or ambient indication --> - <dimen name="keyguard_clock_top_margin">26dp</dimen> - <!-- Large clock maximum font size (dp is intentional, to prevent any further scaling) --> <dimen name="large_clock_text_size">200dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d88428f972cb..343ec4f67964 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2581,4 +2581,12 @@ <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, media app is unknown --> <string name="bt_le_audio_broadcast_dialog_unknown_name">Unknown</string> + <!-- Date format for the Dream Date Complication [CHAR LIMIT=NONE] --> + <string name="dream_date_complication_date_format">EEE, MMM d</string> + + <!-- Time format for the Dream Time Complication for 12-hour time format [CHAR LIMIT=NONE] --> + <string name="dream_time_complication_12_hr_time_format">h:mm</string> + + <!-- Time format for the Dream Time Complication for 24-hour time format [CHAR LIMIT=NONE] --> + <string name="dream_time_complication_24_hr_time_format">kk:mm</string> </resources> diff --git a/packages/SystemUI/screenshot/Android.bp b/packages/SystemUI/screenshot/Android.bp new file mode 100644 index 000000000000..a79fd9040db3 --- /dev/null +++ b/packages/SystemUI/screenshot/Android.bp @@ -0,0 +1,48 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_library { + name: "SystemUIScreenshotLib", + manifest: "AndroidManifest.xml", + + srcs: [ + // All files in this library should be in Kotlin besides some exceptions. + "src/**/*.kt", + + // This file was forked from google3, so exceptionally it can be in Java. + "src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java", + ], + + resource_dirs: [ + "res", + ], + + static_libs: [ + "SystemUI-core", + "androidx.test.espresso.core", + "androidx.appcompat_appcompat", + "platform-screenshot-diff-core", + ], + + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/packages/SystemUI/screenshot/AndroidManifest.xml b/packages/SystemUI/screenshot/AndroidManifest.xml new file mode 100644 index 000000000000..3b703be34e5d --- /dev/null +++ b/packages/SystemUI/screenshot/AndroidManifest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.systemui.testing.screenshot"> + <application> + <activity + android:name="com.android.systemui.testing.screenshot.ScreenshotActivity" + android:exported="true" + android:theme="@style/Theme.SystemUI.Screenshot" /> + </application> + + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> +</manifest> diff --git a/packages/SystemUI/screenshot/res/values/themes.xml b/packages/SystemUI/screenshot/res/values/themes.xml new file mode 100644 index 000000000000..40e50bbb6bbf --- /dev/null +++ b/packages/SystemUI/screenshot/res/values/themes.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <style name="Theme.SystemUI.Screenshot" parent="Theme.SystemUI"> + <item name="android:windowActionBar">false</item> + <item name="android:windowNoTitle">true</item> + + <!-- Make sure that device specific cutouts don't impact the outcome of screenshot tests --> + <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java new file mode 100644 index 000000000000..96ec4c543474 --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.testing.screenshot; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import android.app.UiAutomation; +import android.content.Context; +import android.provider.Settings; +import android.util.Log; + +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.core.content.ContextCompat; +import androidx.test.espresso.Espresso; +import androidx.test.espresso.IdlingRegistry; +import androidx.test.espresso.IdlingResource; + +import org.json.JSONObject; +import org.junit.function.ThrowingRunnable; + +import java.util.HashMap; +import java.util.Map; + +/* + * Note: This file was forked from + * google3/third_party/java_src/android_libs/material_components/screenshot_tests/java/android/ + * support/design/scuba/color/DynamicColorsTestUtils.java. + */ + +/** Utility that helps change the dynamic system colors for testing. */ +@RequiresApi(32) +public class DynamicColorsTestUtils { + + private static final String TAG = DynamicColorsTestUtils.class.getSimpleName(); + + private static final String THEME_CUSTOMIZATION_KEY = "theme_customization_overlay_packages"; + private static final String THEME_CUSTOMIZATION_SYSTEM_PALETTE_KEY = + "android.theme.customization.system_palette"; + + private static final int ORANGE_SYSTEM_SEED_COLOR = 0xA66800; + private static final int ORANGE_EXPECTED_SYSTEM_ACCENT1_600_COLOR = -8235756; + + private DynamicColorsTestUtils() { + } + + /** + * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on an orange + * seed color, and then wait for the change to propagate to the app by comparing + * android.R.color.system_accent1_600 to the expected orange value. + */ + public static void updateSystemColorsToOrange() { + updateSystemColors(ORANGE_SYSTEM_SEED_COLOR, ORANGE_EXPECTED_SYSTEM_ACCENT1_600_COLOR); + } + + /** + * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on the provided + * {@code seedColor}, and then wait for the change to propagate to the app by comparing + * android.R.color.system_accent1_600 to {@code expectedSystemAccent1600}. + */ + public static void updateSystemColors( + @ColorInt int seedColor, @ColorInt int expectedSystemAccent1600) { + Context context = getInstrumentation().getTargetContext(); + + int actualSystemAccent1600 = + ContextCompat.getColor(context, android.R.color.system_accent1_600); + + if (expectedSystemAccent1600 == actualSystemAccent1600) { + String expectedColorString = Integer.toHexString(expectedSystemAccent1600); + Log.d( + TAG, + "Skipped updating system colors since system_accent1_600 is already equal to " + + "expected: " + + expectedColorString); + return; + } + + updateSystemColors(seedColor); + } + + /** + * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on the provided + * {@code seedColor}, and then wait for the change to propagate to the app by checking + * android.R.color.system_accent1_600 for any change. + */ + public static void updateSystemColors(@ColorInt int seedColor) { + Context context = getInstrumentation().getTargetContext(); + + // Initialize system color idling resource with original system_accent1_600 value. + ColorChangeIdlingResource systemColorIdlingResource = + new ColorChangeIdlingResource(context, android.R.color.system_accent1_600); + + // Update system theme color setting to trigger fabricated resource overlay. + runWithShellPermissionIdentity( + () -> + Settings.Secure.putString( + context.getContentResolver(), + THEME_CUSTOMIZATION_KEY, + buildThemeCustomizationString(seedColor))); + + // Wait for system color update to propagate to app. + IdlingRegistry idlingRegistry = IdlingRegistry.getInstance(); + idlingRegistry.register(systemColorIdlingResource); + Espresso.onIdle(); + idlingRegistry.unregister(systemColorIdlingResource); + + Log.d(TAG, + Settings.Secure.getString(context.getContentResolver(), THEME_CUSTOMIZATION_KEY)); + } + + private static String buildThemeCustomizationString(@ColorInt int seedColor) { + String seedColorHex = Integer.toHexString(seedColor); + Map<String, String> themeCustomizationMap = new HashMap<>(); + themeCustomizationMap.put(THEME_CUSTOMIZATION_SYSTEM_PALETTE_KEY, seedColorHex); + return new JSONObject(themeCustomizationMap).toString(); + } + + private static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable runnable) { + UiAutomation uiAutomation = getInstrumentation().getUiAutomation(); + uiAutomation.adoptShellPermissionIdentity(); + try { + runnable.run(); + } catch (Throwable e) { + throw new RuntimeException(e); + } finally { + uiAutomation.dropShellPermissionIdentity(); + } + } + + private static class ColorChangeIdlingResource implements IdlingResource { + + private final Context mContext; + private final int mColorResId; + private final int mInitialColorInt; + + private ResourceCallback mResourceCallback; + private boolean mIdleNow; + + ColorChangeIdlingResource(Context context, @ColorRes int colorResId) { + this.mContext = context; + this.mColorResId = colorResId; + this.mInitialColorInt = ContextCompat.getColor(context, colorResId); + } + + @Override + public String getName() { + return ColorChangeIdlingResource.class.getName(); + } + + @Override + public boolean isIdleNow() { + if (mIdleNow) { + return true; + } + + int currentColorInt = ContextCompat.getColor(mContext, mColorResId); + + String initialColorString = Integer.toHexString(mInitialColorInt); + String currentColorString = Integer.toHexString(currentColorInt); + Log.d(TAG, String.format("Initial=%s, Current=%s", initialColorString, + currentColorString)); + + mIdleNow = currentColorInt != mInitialColorInt; + Log.d(TAG, String.format("idleNow=%b", mIdleNow)); + + if (mIdleNow) { + mResourceCallback.onTransitionToIdle(); + } + return mIdleNow; + } + + @Override + public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { + this.mResourceCallback = resourceCallback; + } + } +} diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt new file mode 100644 index 000000000000..2a55a80eb7f4 --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.testing.screenshot + +import androidx.activity.ComponentActivity + +/** The Activity that is launched and whose content is set for screenshot tests. */ +class ScreenshotActivity : ComponentActivity() diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt new file mode 100644 index 000000000000..363ce10fa36c --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.testing.screenshot + +import android.app.UiModeManager +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.os.UserHandle +import android.view.Display +import android.view.View +import android.view.WindowManagerGlobal +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import platform.test.screenshot.GoldenImagePathManager +import platform.test.screenshot.PathConfig +import platform.test.screenshot.PathElementNoContext +import platform.test.screenshot.ScreenshotTestRule +import platform.test.screenshot.matchers.PixelPerfectMatcher + +/** + * A base rule for screenshot diff tests. + * + * This rules takes care of setting up the activity according to [testSpec] by: + * - emulating the display size and density. + * - setting the dark/light mode. + * - setting the system (Material You) colors to a fixed value. + * + * @see ComposeScreenshotTestRule + * @see ViewScreenshotTestRule + */ +class ScreenshotTestRule(private val testSpec: ScreenshotTestSpec) : TestRule { + private var currentDisplay: DisplaySpec? = null + private var currentGoldenIdentifier: String? = null + + private val pathConfig = + PathConfig( + PathElementNoContext("model", isDir = true) { + currentDisplay?.name ?: error("currentDisplay is null") + }, + ) + private val defaultMatcher = PixelPerfectMatcher() + + private val screenshotRule = + ScreenshotTestRule( + SystemUIGoldenImagePathManager( + pathConfig, + currentGoldenIdentifier = { + currentGoldenIdentifier ?: error("currentGoldenIdentifier is null") + }, + ) + ) + + override fun apply(base: Statement, description: Description): Statement { + // The statement which call beforeTest() before running the test and afterTest() afterwards. + val statement = + object : Statement() { + override fun evaluate() { + try { + beforeTest() + base.evaluate() + } finally { + afterTest() + } + } + } + + return screenshotRule.apply(statement, description) + } + + private fun beforeTest() { + // Update the system colors to a fixed color, so that tests don't depend on the host device + // extracted colors. Note that we don't restore the default device colors at the end of the + // test because changing the colors (and waiting for them to be applied) is costly and makes + // the screenshot tests noticeably slower. + DynamicColorsTestUtils.updateSystemColorsToOrange() + + // Emulate the display size and density. + val display = testSpec.display + val density = display.densityDpi + val wm = WindowManagerGlobal.getWindowManagerService() + val (width, height) = getEmulatedDisplaySize() + wm.setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, density, UserHandle.myUserId()) + wm.setForcedDisplaySize(Display.DEFAULT_DISPLAY, width, height) + + // Force the dark/light theme. + val uiModeManager = + InstrumentationRegistry.getInstrumentation() + .targetContext + .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + uiModeManager.setApplicationNightMode( + if (testSpec.isDarkTheme) { + UiModeManager.MODE_NIGHT_YES + } else { + UiModeManager.MODE_NIGHT_NO + } + ) + } + + private fun afterTest() { + // Reset the density and display size. + val wm = WindowManagerGlobal.getWindowManagerService() + wm.clearForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, UserHandle.myUserId()) + wm.clearForcedDisplaySize(Display.DEFAULT_DISPLAY) + + // Reset the dark/light theme. + val uiModeManager = + InstrumentationRegistry.getInstrumentation() + .targetContext + .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO) + } + + /** + * Compare the content of [view] with the golden image identified by [goldenIdentifier] in the + * context of [testSpec]. + */ + fun screenshotTest(goldenIdentifier: String, view: View) { + val bitmap = drawIntoBitmap(view) + + // Compare bitmap against golden asset. + val isDarkTheme = testSpec.isDarkTheme + val isLandscape = testSpec.isLandscape + val identifierWithSpec = buildString { + append(goldenIdentifier) + if (isDarkTheme) append("_dark") + if (isLandscape) append("_landscape") + } + + // TODO(b/230832101): Provide a way to pass a PathConfig and override the file name on + // device to assertBitmapAgainstGolden instead? + currentDisplay = testSpec.display + currentGoldenIdentifier = goldenIdentifier + screenshotRule.assertBitmapAgainstGolden(bitmap, identifierWithSpec, defaultMatcher) + currentDisplay = null + currentGoldenIdentifier = goldenIdentifier + } + + /** Draw [view] into a [Bitmap]. */ + private fun drawIntoBitmap(view: View): Bitmap { + val bitmap = + Bitmap.createBitmap( + view.measuredWidth, + view.measuredHeight, + Bitmap.Config.ARGB_8888, + ) + val canvas = Canvas(bitmap) + view.draw(canvas) + return bitmap + } + + /** Get the emulated display size for [testSpec]. */ + private fun getEmulatedDisplaySize(): Pair<Int, Int> { + val display = testSpec.display + val isPortraitNaturalPosition = display.width < display.height + return if (testSpec.isLandscape) { + if (isPortraitNaturalPosition) { + display.height to display.width + } else { + display.width to display.height + } + } else { + if (isPortraitNaturalPosition) { + display.width to display.height + } else { + display.height to display.width + } + } + } +} + +private class SystemUIGoldenImagePathManager( + pathConfig: PathConfig, + private val currentGoldenIdentifier: () -> String, +) : + GoldenImagePathManager( + appContext = InstrumentationRegistry.getInstrumentation().context, + deviceLocalPath = + InstrumentationRegistry.getInstrumentation() + .targetContext + .filesDir + .absolutePath + .toString() + "/sysui_screenshots", + pathConfig = pathConfig, + ) { + // This string is appended to all actual/expected screenshots on the device. We append the + // golden identifier so that our pull_golden.py scripts can map a screenshot on device to its + // asset (and automatically update it, if necessary). + override fun toString() = currentGoldenIdentifier() +} diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt new file mode 100644 index 000000000000..7fc624554738 --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.testing.screenshot + +/** The specification of a device display to be used in a screenshot test. */ +data class DisplaySpec( + val name: String, + val width: Int, + val height: Int, + val densityDpi: Int, +) + +/** The specification of a screenshot diff test. */ +class ScreenshotTestSpec( + val display: DisplaySpec, + val isDarkTheme: Boolean = false, + val isLandscape: Boolean = false, +) { + companion object { + /** + * Return a list of [ScreenshotTestSpec] for each of the [displays]. + * + * If [isDarkTheme] is null, this will create a spec for both light and dark themes, for + * each of the orientation. + * + * If [isLandscape] is null, this will create a spec for both portrait and landscape, for + * each of the light/dark themes. + */ + fun forDisplays( + vararg displays: DisplaySpec, + isDarkTheme: Boolean? = null, + isLandscape: Boolean? = null, + ): List<ScreenshotTestSpec> { + return displays.flatMap { display -> + buildList { + fun addDisplay(isLandscape: Boolean) { + if (isDarkTheme != true) { + add(ScreenshotTestSpec(display, isDarkTheme = false, isLandscape)) + } + + if (isDarkTheme != false) { + add(ScreenshotTestSpec(display, isDarkTheme = true, isLandscape)) + } + } + + if (isLandscape != true) { + addDisplay(isLandscape = false) + } + + if (isLandscape != false) { + addDisplay(isLandscape = true) + } + } + } + } + } + + override fun toString(): String = buildString { + // This string is appended to PNGs stored in the device, so let's keep it simple. + append(display.name) + if (isDarkTheme) append("_dark") + if (isLandscape) append("_landscape") + } +} diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt new file mode 100644 index 000000000000..2c3ff2c75c72 --- /dev/null +++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt @@ -0,0 +1,51 @@ +package com.android.systemui.testing.screenshot + +import android.app.Activity +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams +import androidx.test.ext.junit.rules.ActivityScenarioRule +import org.junit.Assert.assertEquals +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** A rule for View screenshot diff tests. */ +class ViewScreenshotTestRule(testSpec: ScreenshotTestSpec) : TestRule { + private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java) + private val screenshotRule = ScreenshotTestRule(testSpec) + + private val delegate = RuleChain.outerRule(screenshotRule).around(activityRule) + + override fun apply(base: Statement, description: Description): Statement { + return delegate.apply(base, description) + } + + /** + * Compare the content of [view] with the golden image identified by [goldenIdentifier] in the + * context of [testSpec]. + */ + fun screenshotTest( + goldenIdentifier: String, + layoutParams: LayoutParams = + LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT), + view: (Activity) -> View, + ) { + activityRule.scenario.onActivity { activity -> + // Make sure that the activity draws full screen and fits the whole display instead of + // the system bars. + activity.window.setDecorFitsSystemWindows(false) + activity.setContentView(view(activity), layoutParams) + } + + // We call onActivity again because it will make sure that our Activity is done measuring, + // laying out and drawing its content (that we set in the previous onActivity lambda). + activityRule.scenario.onActivity { activity -> + // Check that the content is what we expected. + val content = activity.requireViewById<ViewGroup>(android.R.id.content) + assertEquals(1, content.childCount) + screenshotRule.screenshotTest(goldenIdentifier, content.getChildAt(0)) + } + } +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt index 4b8b46d54848..06247c6c9523 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -54,6 +54,8 @@ open class ClockRegistry( fun onClockChanged() } + var isEnabled: Boolean = false + private val gson = Gson() private val availableClocks = mutableMapOf<ClockId, ClockInfo>() private val clockChangeListeners = mutableListOf<ClockChangeListener>() @@ -88,6 +90,12 @@ open class ClockRegistry( init { connectClocks(defaultClockProvider) + if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) { + throw IllegalArgumentException( + "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID" + ) + } + pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java) context.contentResolver.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), @@ -133,7 +141,12 @@ open class ClockRegistry( } } - fun getClocks(): List<ClockMetadata> = availableClocks.map { (_, clock) -> clock.metadata } + fun getClocks(): List<ClockMetadata> { + if (!isEnabled) { + return listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata) + } + return availableClocks.map { (_, clock) -> clock.metadata } + } fun getClockThumbnail(clockId: ClockId): Drawable? = availableClocks[clockId]?.provider?.getClockThumbnail(clockId) @@ -148,7 +161,7 @@ open class ClockRegistry( fun createCurrentClock(): Clock { val clockId = currentClockId - if (!clockId.isNullOrEmpty()) { + if (isEnabled && clockId.isNotEmpty()) { val clock = createClock(clockId) if (clock != null) { return clock diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java index 302ba8444f05..e629f749ee96 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java @@ -105,7 +105,7 @@ public class RemoteAnimationAdapterCompat { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { remoteAnimationAdapter.onAnimationCancelled(); } }; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java index 4ce110bf7c47..249133a9a63b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java @@ -141,8 +141,10 @@ public class RemoteAnimationTargetCompat { final int mode = change.getMode(); t.reparent(leash, info.getRootLeash()); - t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x, - change.getStartAbsBounds().top - info.getRootOffset().y); + final Rect absBounds = + (mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds(); + t.setPosition(leash, absBounds.left - info.getRootOffset().x, + absBounds.top - info.getRootOffset().y); // Put all the OPEN/SHOW on top if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index d566f49c04ff..eeab538932c0 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -37,6 +37,8 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.plugins.Clock; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -118,7 +120,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS SecureSettings secureSettings, @Main Executor uiExecutor, DumpManager dumpManager, - ClockEventController clockEventController) { + ClockEventController clockEventController, + FeatureFlags featureFlags) { super(keyguardClockSwitch); mStatusBarStateController = statusBarStateController; mClockRegistry = clockRegistry; @@ -131,6 +134,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mDumpManager = dumpManager; mClockEventController = clockEventController; + mClockRegistry.setEnabled(featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS)); mClockChangedListener = () -> { setClock(mClockRegistry.createCurrentClock()); }; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 11519bf38516..8fb622a8c55e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -94,6 +94,7 @@ import com.android.systemui.util.settings.GlobalSettings; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; public class KeyguardSecurityContainer extends FrameLayout { static final int USER_TYPE_PRIMARY = 1; @@ -128,12 +129,12 @@ public class KeyguardSecurityContainer extends FrameLayout { private static final long IME_DISAPPEAR_DURATION_MS = 125; - // The duration of the animation to switch bouncer sides. - private static final long BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS = 500; + // The duration of the animation to switch security sides. + private static final long SECURITY_SHIFT_ANIMATION_DURATION_MS = 500; - // How much of the switch sides animation should be dedicated to fading the bouncer out. The + // How much of the switch sides animation should be dedicated to fading the security out. The // remainder will fade it back in again. - private static final float BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION = 0.2f; + private static final float SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION = 0.2f; @VisibleForTesting KeyguardSecurityViewFlipper mSecurityViewFlipper; @@ -796,12 +797,16 @@ public class KeyguardSecurityContainer extends FrameLayout { * screen devices */ abstract static class SidedSecurityMode implements ViewMode { + @Nullable private ValueAnimator mRunningSecurityShiftAnimator; + private KeyguardSecurityViewFlipper mViewFlipper; private ViewGroup mView; private GlobalSettings mGlobalSettings; private int mDefaultSideSetting; - public void init(ViewGroup v, GlobalSettings globalSettings, boolean leftAlignedByDefault) { + public void init(ViewGroup v, KeyguardSecurityViewFlipper viewFlipper, + GlobalSettings globalSettings, boolean leftAlignedByDefault) { mView = v; + mViewFlipper = viewFlipper; mGlobalSettings = globalSettings; mDefaultSideSetting = leftAlignedByDefault ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT @@ -841,6 +846,127 @@ public class KeyguardSecurityContainer extends FrameLayout { protected abstract void updateSecurityViewLocation(boolean leftAlign, boolean animate); + protected void translateSecurityViewLocation(boolean leftAlign, boolean animate) { + translateSecurityViewLocation(leftAlign, animate, i -> {}); + } + + /** + * Moves the inner security view to the correct location with animation. This is triggered + * when the user double taps on the side of the screen that is not currently occupied by + * the security view. + */ + protected void translateSecurityViewLocation(boolean leftAlign, boolean animate, + Consumer<Float> securityAlphaListener) { + if (mRunningSecurityShiftAnimator != null) { + mRunningSecurityShiftAnimator.cancel(); + mRunningSecurityShiftAnimator = null; + } + + int targetTranslation = leftAlign + ? 0 : mView.getMeasuredWidth() - mViewFlipper.getWidth(); + + if (animate) { + // This animation is a bit fun to implement. The bouncer needs to move, and fade + // in/out at the same time. The issue is, the bouncer should only move a short + // amount (120dp or so), but obviously needs to go from one side of the screen to + // the other. This needs a pretty custom animation. + // + // This works as follows. It uses a ValueAnimation to simply drive the animation + // progress. This animator is responsible for both the translation of the bouncer, + // and the current fade. It will fade the bouncer out while also moving it along the + // 120dp path. Once the bouncer is fully faded out though, it will "snap" the + // bouncer closer to its destination, then fade it back in again. The effect is that + // the bouncer will move from 0 -> X while fading out, then + // (destination - X) -> destination while fading back in again. + // TODO(b/208250221): Make this animation properly abortable. + Interpolator positionInterpolator = AnimationUtils.loadInterpolator( + mView.getContext(), android.R.interpolator.fast_out_extra_slow_in); + Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN; + Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN; + + mRunningSecurityShiftAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); + mRunningSecurityShiftAnimator.setDuration(SECURITY_SHIFT_ANIMATION_DURATION_MS); + mRunningSecurityShiftAnimator.setInterpolator(Interpolators.LINEAR); + + int initialTranslation = (int) mViewFlipper.getTranslationX(); + int totalTranslation = (int) mView.getResources().getDimension( + R.dimen.security_shift_animation_translation); + + final boolean shouldRestoreLayerType = mViewFlipper.hasOverlappingRendering() + && mViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE; + if (shouldRestoreLayerType) { + mViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null); + } + + float initialAlpha = mViewFlipper.getAlpha(); + + mRunningSecurityShiftAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mRunningSecurityShiftAnimator = null; + } + }); + mRunningSecurityShiftAnimator.addUpdateListener(animation -> { + float switchPoint = SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION; + boolean isFadingOut = animation.getAnimatedFraction() < switchPoint; + + int currentTranslation = (int) (positionInterpolator.getInterpolation( + animation.getAnimatedFraction()) * totalTranslation); + int translationRemaining = totalTranslation - currentTranslation; + + // Flip the sign if we're going from right to left. + if (leftAlign) { + currentTranslation = -currentTranslation; + translationRemaining = -translationRemaining; + } + + float opacity; + if (isFadingOut) { + // The bouncer fades out over the first X%. + float fadeOutFraction = MathUtils.constrainedMap( + /* rangeMin= */1.0f, + /* rangeMax= */0.0f, + /* valueMin= */0.0f, + /* valueMax= */switchPoint, + animation.getAnimatedFraction()); + opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction); + + // When fading out, the alpha needs to start from the initial opacity of the + // view flipper, otherwise we get a weird bit of jank as it ramps back to + // 100%. + mViewFlipper.setAlpha(opacity * initialAlpha); + + // Animate away from the source. + mViewFlipper.setTranslationX(initialTranslation + currentTranslation); + } else { + // And in again over the remaining (100-X)%. + float fadeInFraction = MathUtils.constrainedMap( + /* rangeMin= */0.0f, + /* rangeMax= */1.0f, + /* valueMin= */switchPoint, + /* valueMax= */1.0f, + animation.getAnimatedFraction()); + + opacity = fadeInInterpolator.getInterpolation(fadeInFraction); + mViewFlipper.setAlpha(opacity); + + // Fading back in, animate towards the destination. + mViewFlipper.setTranslationX(targetTranslation - translationRemaining); + } + securityAlphaListener.accept(opacity); + + if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) { + mViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null); + } + }); + + mRunningSecurityShiftAnimator.start(); + } else { + mViewFlipper.setTranslationX(targetTranslation); + } + } + + boolean isLeftAligned() { return mGlobalSettings.getInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE, mDefaultSideSetting) @@ -899,12 +1025,15 @@ public class KeyguardSecurityContainer extends FrameLayout { private UserSwitcherController.UserSwitchCallback mUserSwitchCallback = this::setupUserSwitcher; + private float mAnimationLastAlpha = 1f; + private boolean mAnimationWaitsToShift = true; + @Override public void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings, @NonNull KeyguardSecurityViewFlipper viewFlipper, @NonNull FalsingManager falsingManager, @NonNull UserSwitcherController userSwitcherController) { - init(v, globalSettings, /* leftAlignedByDefault= */false); + init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */false); mView = v; mViewFlipper = viewFlipper; mFalsingManager = falsingManager; @@ -918,9 +1047,7 @@ public class KeyguardSecurityContainer extends FrameLayout { true); mUserSwitcherViewGroup = mView.findViewById(R.id.keyguard_bouncer_user_switcher); } - updateSecurityViewLocation(); - mUserSwitcher = mView.findViewById(R.id.user_switcher_header); setupUserSwitcher(); mUserSwitcherController.addUserSwitchCallback(mUserSwitchCallback); @@ -1120,22 +1247,61 @@ public class KeyguardSecurityContainer extends FrameLayout { } public void updateSecurityViewLocation(boolean leftAlign, boolean animate) { - int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans); + setYTranslation(); + setGravity(); + setXTranslation(leftAlign, animate); + } + private void setXTranslation(boolean leftAlign, boolean animate) { + if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { + mUserSwitcherViewGroup.setTranslationX(0); + mViewFlipper.setTranslationX(0); + } else { + int switcherTargetTranslation = leftAlign + ? mView.getMeasuredWidth() - mViewFlipper.getWidth() : 0; + if (animate) { + mAnimationWaitsToShift = true; + mAnimationLastAlpha = 1f; + translateSecurityViewLocation(leftAlign, animate, securityAlpha -> { + // During the animation security view fades out - alpha goes from 1 to + // (almost) 0 - and then fades in - alpha grows back to 1. + // If new alpha is bigger than previous one it means we're at inflection + // point and alpha is zero or almost zero. That's when we want to do + // translation of user switcher, so that it's not visible to the user. + boolean fullyFadeOut = securityAlpha == 0.0f + || securityAlpha > mAnimationLastAlpha; + if (fullyFadeOut && mAnimationWaitsToShift) { + mUserSwitcherViewGroup.setTranslationX(switcherTargetTranslation); + mAnimationWaitsToShift = false; + } + mUserSwitcherViewGroup.setAlpha(securityAlpha); + mAnimationLastAlpha = securityAlpha; + }); + } else { + translateSecurityViewLocation(leftAlign, animate); + mUserSwitcherViewGroup.setTranslationX(switcherTargetTranslation); + } + } + + } + + private void setGravity() { if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { - updateViewGravity(mViewFlipper, Gravity.CENTER_HORIZONTAL); updateViewGravity(mUserSwitcherViewGroup, Gravity.CENTER_HORIZONTAL); + updateViewGravity(mViewFlipper, Gravity.CENTER_HORIZONTAL); + } else { + // horizontal gravity is the same because we translate these views anyway + updateViewGravity(mViewFlipper, Gravity.LEFT | Gravity.BOTTOM); + updateViewGravity(mUserSwitcherViewGroup, Gravity.LEFT | Gravity.CENTER_VERTICAL); + } + } + private void setYTranslation() { + int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans); + if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { mUserSwitcherViewGroup.setTranslationY(yTrans); mViewFlipper.setTranslationY(-yTrans); } else { - int securityHorizontalGravity = leftAlign ? Gravity.LEFT : Gravity.RIGHT; - int userSwitcherHorizontalGravity = - securityHorizontalGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT; - updateViewGravity(mViewFlipper, securityHorizontalGravity | Gravity.BOTTOM); - updateViewGravity(mUserSwitcherViewGroup, - userSwitcherHorizontalGravity | Gravity.CENTER_VERTICAL); - // Attempt to reposition a bit higher to make up for this frame being a bit lower // on the device mUserSwitcherViewGroup.setTranslationY(-yTrans); @@ -1155,7 +1321,6 @@ public class KeyguardSecurityContainer extends FrameLayout { * between alternate sides of the display. */ static class OneHandedViewMode extends SidedSecurityMode { - @Nullable private ValueAnimator mRunningOneHandedAnimator; private ViewGroup mView; private KeyguardSecurityViewFlipper mViewFlipper; @@ -1164,7 +1329,7 @@ public class KeyguardSecurityContainer extends FrameLayout { @NonNull KeyguardSecurityViewFlipper viewFlipper, @NonNull FalsingManager falsingManager, @NonNull UserSwitcherController userSwitcherController) { - init(v, globalSettings, /* leftAlignedByDefault= */true); + init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */true); mView = v; mViewFlipper = viewFlipper; @@ -1205,117 +1370,8 @@ public class KeyguardSecurityContainer extends FrameLayout { updateSecurityViewLocation(isLeftAligned(), /* animate= */false); } - /** - * Moves the inner security view to the correct location (in one handed mode) with - * animation. This is triggered when the user double taps on the side of the screen that is - * not currently occupied by the security view. - */ protected void updateSecurityViewLocation(boolean leftAlign, boolean animate) { - if (mRunningOneHandedAnimator != null) { - mRunningOneHandedAnimator.cancel(); - mRunningOneHandedAnimator = null; - } - - int targetTranslation = leftAlign - ? 0 : (int) (mView.getMeasuredWidth() - mViewFlipper.getWidth()); - - if (animate) { - // This animation is a bit fun to implement. The bouncer needs to move, and fade - // in/out at the same time. The issue is, the bouncer should only move a short - // amount (120dp or so), but obviously needs to go from one side of the screen to - // the other. This needs a pretty custom animation. - // - // This works as follows. It uses a ValueAnimation to simply drive the animation - // progress. This animator is responsible for both the translation of the bouncer, - // and the current fade. It will fade the bouncer out while also moving it along the - // 120dp path. Once the bouncer is fully faded out though, it will "snap" the - // bouncer closer to its destination, then fade it back in again. The effect is that - // the bouncer will move from 0 -> X while fading out, then - // (destination - X) -> destination while fading back in again. - // TODO(b/208250221): Make this animation properly abortable. - Interpolator positionInterpolator = AnimationUtils.loadInterpolator( - mView.getContext(), android.R.interpolator.fast_out_extra_slow_in); - Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN; - Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN; - - mRunningOneHandedAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); - mRunningOneHandedAnimator.setDuration(BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS); - mRunningOneHandedAnimator.setInterpolator(Interpolators.LINEAR); - - int initialTranslation = (int) mViewFlipper.getTranslationX(); - int totalTranslation = (int) mView.getResources().getDimension( - R.dimen.one_handed_bouncer_move_animation_translation); - - final boolean shouldRestoreLayerType = mViewFlipper.hasOverlappingRendering() - && mViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE; - if (shouldRestoreLayerType) { - mViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null); - } - - float initialAlpha = mViewFlipper.getAlpha(); - - mRunningOneHandedAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mRunningOneHandedAnimator = null; - } - }); - mRunningOneHandedAnimator.addUpdateListener(animation -> { - float switchPoint = BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION; - boolean isFadingOut = animation.getAnimatedFraction() < switchPoint; - - int currentTranslation = (int) (positionInterpolator.getInterpolation( - animation.getAnimatedFraction()) * totalTranslation); - int translationRemaining = totalTranslation - currentTranslation; - - // Flip the sign if we're going from right to left. - if (leftAlign) { - currentTranslation = -currentTranslation; - translationRemaining = -translationRemaining; - } - - if (isFadingOut) { - // The bouncer fades out over the first X%. - float fadeOutFraction = MathUtils.constrainedMap( - /* rangeMin= */1.0f, - /* rangeMax= */0.0f, - /* valueMin= */0.0f, - /* valueMax= */switchPoint, - animation.getAnimatedFraction()); - float opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction); - - // When fading out, the alpha needs to start from the initial opacity of the - // view flipper, otherwise we get a weird bit of jank as it ramps back to - // 100%. - mViewFlipper.setAlpha(opacity * initialAlpha); - - // Animate away from the source. - mViewFlipper.setTranslationX(initialTranslation + currentTranslation); - } else { - // And in again over the remaining (100-X)%. - float fadeInFraction = MathUtils.constrainedMap( - /* rangeMin= */0.0f, - /* rangeMax= */1.0f, - /* valueMin= */switchPoint, - /* valueMax= */1.0f, - animation.getAnimatedFraction()); - - float opacity = fadeInInterpolator.getInterpolation(fadeInFraction); - mViewFlipper.setAlpha(opacity); - - // Fading back in, animate towards the destination. - mViewFlipper.setTranslationX(targetTranslation - translationRemaining); - } - - if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) { - mViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null); - } - }); - - mRunningOneHandedAnimator.start(); - } else { - mViewFlipper.setTranslationX(targetTranslation); - } + translateSecurityViewLocation(leftAlign, animate); } } } diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt index 61b1b66338e8..c5955860aebf 100644 --- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt @@ -27,12 +27,15 @@ import android.graphics.Matrix import android.graphics.Paint import android.graphics.Path import android.graphics.RectF +import android.hardware.biometrics.BiometricSourceType import android.view.View import androidx.core.graphics.ColorUtils import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.settingslib.Utils import com.android.systemui.animation.Interpolators import com.android.systemui.plugins.statusbar.StatusBarStateController +import java.util.concurrent.Executor /** * When the face is enrolled, we use this view to show the face scanning animation and the camera @@ -42,7 +45,8 @@ class FaceScanningOverlay( context: Context, pos: Int, val statusBarStateController: StatusBarStateController, - val keyguardUpdateMonitor: KeyguardUpdateMonitor + val keyguardUpdateMonitor: KeyguardUpdateMonitor, + val mainExecutor: Executor ) : ScreenDecorations.DisplayCutoutView(context, pos) { private var showScanningAnim = false private val rimPaint = Paint() @@ -54,11 +58,26 @@ class FaceScanningOverlay( com.android.systemui.R.attr.wallpaperTextColorAccent) private var cameraProtectionAnimator: ValueAnimator? = null var hideOverlayRunnable: Runnable? = null + var faceAuthSucceeded = false init { visibility = View.INVISIBLE // only show this view when face scanning is happening } + override fun onAttachedToWindow() { + super.onAttachedToWindow() + mainExecutor.execute { + keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + mainExecutor.execute { + keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) + } + } + override fun setColor(color: Int) { cameraProtectionColor = color invalidate() @@ -108,7 +127,6 @@ class FaceScanningOverlay( if (showScanningAnimNow == showScanningAnim) { return } - showScanningAnim = showScanningAnimNow updateProtectionBoundingPath() // Delay the relayout until the end of the animation when hiding, @@ -120,13 +138,20 @@ class FaceScanningOverlay( cameraProtectionAnimator?.cancel() cameraProtectionAnimator = ValueAnimator.ofFloat(cameraProtectionProgress, - if (showScanningAnimNow) 1.0f else HIDDEN_CAMERA_PROTECTION_SCALE).apply { - startDelay = if (showScanningAnim) 0 else PULSE_DISAPPEAR_DURATION - duration = if (showScanningAnim) PULSE_APPEAR_DURATION else - CAMERA_PROTECTION_DISAPPEAR_DURATION - interpolator = if (showScanningAnim) Interpolators.STANDARD else - Interpolators.EMPHASIZED - + if (showScanningAnimNow) SHOW_CAMERA_PROTECTION_SCALE + else HIDDEN_CAMERA_PROTECTION_SCALE).apply { + startDelay = + if (showScanningAnim) 0 + else if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION + else PULSE_ERROR_DISAPPEAR_DURATION + duration = + if (showScanningAnim) CAMERA_PROTECTION_APPEAR_DURATION + else if (faceAuthSucceeded) CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION + else CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION + interpolator = + if (showScanningAnim) Interpolators.STANDARD_ACCELERATE + else if (faceAuthSucceeded) Interpolators.STANDARD + else Interpolators.STANDARD_DECELERATE addUpdateListener(ValueAnimator.AnimatorUpdateListener { animation: ValueAnimator -> cameraProtectionProgress = animation.animatedValue as Float @@ -143,47 +168,73 @@ class FaceScanningOverlay( } } }) - start() } rimAnimator?.cancel() rimAnimator = AnimatorSet().apply { - val rimAppearOrDisappearAnimator = ValueAnimator.ofFloat(rimProgress, - if (showScanningAnim) PULSE_RADIUS_OUT else (PULSE_RADIUS_IN * 1.15f)).apply { - duration = if (showScanningAnim) PULSE_APPEAR_DURATION else PULSE_DISAPPEAR_DURATION - interpolator = Interpolators.STANDARD - addUpdateListener(ValueAnimator.AnimatorUpdateListener { - animation: ValueAnimator -> - rimProgress = animation.animatedValue as Float - invalidate() - }) - } if (showScanningAnim) { - // appear and then pulse in/out - playSequentially(rimAppearOrDisappearAnimator, + val rimAppearAnimator = ValueAnimator.ofFloat(SHOW_CAMERA_PROTECTION_SCALE, + PULSE_RADIUS_OUT).apply { + duration = PULSE_APPEAR_DURATION + interpolator = Interpolators.STANDARD_DECELERATE + addUpdateListener(ValueAnimator.AnimatorUpdateListener { + animation: ValueAnimator -> + rimProgress = animation.animatedValue as Float + invalidate() + }) + } + + // animate in camera protection, rim, and then pulse in/out + playSequentially(cameraProtectionAnimator, rimAppearAnimator, createPulseAnimator(), createPulseAnimator(), createPulseAnimator(), createPulseAnimator(), createPulseAnimator(), createPulseAnimator()) } else { - val opacityAnimator = ValueAnimator.ofInt(255, 0).apply { - duration = PULSE_DISAPPEAR_DURATION - interpolator = Interpolators.LINEAR + val rimDisappearAnimator = ValueAnimator.ofFloat( + rimProgress, + if (faceAuthSucceeded) PULSE_RADIUS_SUCCESS + else SHOW_CAMERA_PROTECTION_SCALE + ).apply { + duration = + if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION + else PULSE_ERROR_DISAPPEAR_DURATION + interpolator = + if (faceAuthSucceeded) Interpolators.STANDARD_DECELERATE + else Interpolators.STANDARD addUpdateListener(ValueAnimator.AnimatorUpdateListener { animation: ValueAnimator -> - rimPaint.alpha = animation.animatedValue as Int + rimProgress = animation.animatedValue as Float invalidate() }) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rimProgress = HIDDEN_RIM_SCALE + invalidate() + } + }) } - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - rimProgress = HIDDEN_RIM_SCALE - rimPaint.alpha = 255 - invalidate() + if (faceAuthSucceeded) { + val successOpacityAnimator = ValueAnimator.ofInt(255, 0).apply { + duration = PULSE_SUCCESS_DISAPPEAR_DURATION + interpolator = Interpolators.LINEAR + addUpdateListener(ValueAnimator.AnimatorUpdateListener { + animation: ValueAnimator -> + rimPaint.alpha = animation.animatedValue as Int + invalidate() + }) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rimPaint.alpha = 255 + invalidate() + } + }) } - }) - - // disappear - playTogether(rimAppearOrDisappearAnimator, opacityAnimator) + val rimSuccessAnimator = AnimatorSet() + rimSuccessAnimator.playTogether(rimDisappearAnimator, successOpacityAnimator) + playTogether(rimSuccessAnimator, cameraProtectionAnimator) + } else { + playTogether(rimDisappearAnimator, cameraProtectionAnimator) + } } addListener(object : AnimatorListenerAdapter() { @@ -253,15 +304,72 @@ class FaceScanningOverlay( } } + private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { + override fun onBiometricAuthenticated( + userId: Int, + biometricSourceType: BiometricSourceType?, + isStrongBiometric: Boolean + ) { + if (biometricSourceType == BiometricSourceType.FACE) { + post { + faceAuthSucceeded = true + enableShowProtection(true) + } + } + } + + override fun onBiometricAcquired( + biometricSourceType: BiometricSourceType?, + acquireInfo: Int + ) { + if (biometricSourceType == BiometricSourceType.FACE) { + post { + faceAuthSucceeded = false // reset + } + } + } + + override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) { + if (biometricSourceType == BiometricSourceType.FACE) { + post { + faceAuthSucceeded = false + enableShowProtection(false) + } + } + } + + override fun onBiometricError( + msgId: Int, + errString: String?, + biometricSourceType: BiometricSourceType? + ) { + if (biometricSourceType == BiometricSourceType.FACE) { + post { + faceAuthSucceeded = false + enableShowProtection(false) + } + } + } + } + companion object { private const val HIDDEN_RIM_SCALE = HIDDEN_CAMERA_PROTECTION_SCALE + private const val SHOW_CAMERA_PROTECTION_SCALE = 1f + + private const val PULSE_RADIUS_IN = 1.1f + private const val PULSE_RADIUS_OUT = 1.125f + private const val PULSE_RADIUS_SUCCESS = 1.25f + + private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L + private const val PULSE_APPEAR_DURATION = 250L // without start delay - private const val PULSE_APPEAR_DURATION = 350L private const val PULSE_DURATION_INWARDS = 500L private const val PULSE_DURATION_OUTWARDS = 500L - private const val PULSE_DISAPPEAR_DURATION = 850L - private const val CAMERA_PROTECTION_DISAPPEAR_DURATION = 700L // excluding start delay - private const val PULSE_RADIUS_IN = 1.15f - private const val PULSE_RADIUS_OUT = 1.25f + + private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L + private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay + + private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L + private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay } } diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt index adc0096eed37..81d3d6caebd7 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt @@ -32,10 +32,12 @@ import android.widget.FrameLayout import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.FaceScanningOverlay import com.android.systemui.biometrics.AuthController +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.plugins.statusbar.StatusBarStateController +import java.util.concurrent.Executor import javax.inject.Inject @SysUISingleton @@ -44,6 +46,7 @@ class FaceScanningProviderFactory @Inject constructor( private val context: Context, private val statusBarStateController: StatusBarStateController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + @Main private val mainExecutor: Executor, private val featureFlags: FeatureFlags ) : DecorProviderFactory() { private val display = context.display @@ -82,7 +85,9 @@ class FaceScanningProviderFactory @Inject constructor( bound.baseOnRotation0(displayInfo.rotation), authController, statusBarStateController, - keyguardUpdateMonitor) + keyguardUpdateMonitor, + mainExecutor + ) ) } } @@ -102,7 +107,8 @@ class FaceScanningOverlayProviderImpl( override val alignedBound: Int, private val authController: AuthController, private val statusBarStateController: StatusBarStateController, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val mainExecutor: Executor ) : BoundDecorProvider() { override val viewId: Int = com.android.systemui.R.id.face_scanning_anim @@ -127,7 +133,9 @@ class FaceScanningOverlayProviderImpl( context, alignedBound, statusBarStateController, - keyguardUpdateMonitor) + keyguardUpdateMonitor, + mainExecutor + ) view.id = viewId FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT).let { diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index ab6fe89a0588..56bb53b567be 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -60,6 +60,9 @@ public class Flags { public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS = new ResourceBooleanFlag(108, R.bool.config_notificationToContents); + public static final BooleanFlag REMOVE_UNRANKED_NOTIFICATIONS = + new BooleanFlag(109, false); + /***************************************/ // 200 - keyguard/lockscreen @@ -81,12 +84,15 @@ public class Flags { public static final ResourceBooleanFlag FACE_SCANNING_ANIM = new ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation); + /** * Whether the KeyguardBottomArea(View|Controller) should use the modern architecture or the old * one. */ public static final BooleanFlag MODERN_BOTTOM_AREA = new BooleanFlag(206, false); + public static final BooleanFlag LOCKSCREEN_CUSTOM_CLOCKS = new BooleanFlag(207, false); + /***************************************/ // 300 - power menu public static final BooleanFlag POWER_MENU_LITE = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index d71956d054be..95b3b3f6452f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -263,7 +263,7 @@ public class KeyguardService extends Service { // already finished (or not started yet), so do nothing. return; } - runner.onAnimationCancelled(); + runner.onAnimationCancelled(false /* isKeyguardOccluded */); origFinishCB.onTransitionFinished(null /* wct */, null /* t */); } catch (RemoteException e) { // nothing, we'll just let it finish on its own I guess. @@ -396,7 +396,7 @@ public class KeyguardService extends Service { } @Override // Binder interface - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { mKeyguardViewMediator.cancelKeyguardExitAnimation(); } }; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index de8b0cfa495a..4ee89854987c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -911,12 +911,12 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, private final Matrix mUnoccludeMatrix = new Matrix(); @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { if (mUnoccludeAnimator != null) { mUnoccludeAnimator.cancel(); } - setOccluded(false /* isOccluded */, false /* animate */); + setOccluded(isKeyguardOccluded, false /* animate */); Log.d(TAG, "Unocclude animation cancelled. Occluded state is now: " + mOccluded); } @@ -3169,9 +3169,9 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, } @Override - public void onAnimationCancelled() throws RemoteException { + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { if (mRunner != null) { - mRunner.onAnimationCancelled(); + mRunner.onAnimationCancelled(isKeyguardOccluded); } } @@ -3212,8 +3212,8 @@ public class KeyguardViewMediator extends CoreStartable implements Dumpable, } @Override - public void onAnimationCancelled() throws RemoteException { - super.onAnimationCancelled(); + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { + super.onAnimationCancelled(isKeyguardOccluded); Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: " + mOccluded); } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index 30ba476abce2..88a1b17e37fe 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -1096,7 +1096,7 @@ class MediaDataManager( fun onNotificationRemoved(key: String) { Assert.isMainThread() val removed = mediaEntries.remove(key) - if (useMediaResumption && removed?.resumeAction != null && removed?.isLocalSession()) { + if (useMediaResumption && removed?.resumeAction != null && removed.isLocalSession()) { Log.d(TAG, "Not removing $key because resumable") // Move to resume key (aka package name) if that key doesn't already exist. val resumeAction = getResumeMediaAction(removed.resumeAction!!) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index ed5c1933af25..2f732de50ea1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -23,6 +23,7 @@ import android.annotation.IntDef import android.content.Context import android.content.res.Configuration import android.graphics.Rect +import android.util.Log import android.util.MathUtils import android.view.View import android.view.ViewGroup @@ -48,6 +49,8 @@ import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.traceSection import javax.inject.Inject +private val TAG: String = MediaHierarchyManager::class.java.simpleName + /** * Similarly to isShown but also excludes views that have 0 alpha */ @@ -964,6 +967,14 @@ class MediaHierarchyManager @Inject constructor( top, left + currentBounds.width(), top + currentBounds.height()) + + if (mediaFrame.childCount > 0) { + val child = mediaFrame.getChildAt(0) + if (mediaFrame.height < child.height) { + Log.wtf(TAG, "mediaFrame height is too small for child: " + + "${mediaFrame.height} vs ${child.height}") + } + } } if (isCrossFadeAnimatorRunning) { // When cross-fading with an animation, we only notify the media carousel of the diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index 2c5d1b95654e..e6116c16ec4a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -69,11 +69,13 @@ public abstract class MediaOutputBaseAdapter extends View mHolderView; boolean mIsDragging; int mCurrentActivePosition; + private boolean mIsInitVolumeFirstTime; public MediaOutputBaseAdapter(MediaOutputController controller) { mController = controller; mIsDragging = false; mCurrentActivePosition = -1; + mIsInitVolumeFirstTime = true; } @Override @@ -275,7 +277,7 @@ public abstract class MediaOutputBaseAdapter extends mSeekBar.setMaxVolume(device.getMaxVolume()); final int currentVolume = device.getCurrentVolume(); if (mSeekBar.getVolume() != currentVolume) { - if (isCurrentSeekbarInvisible) { + if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) { animateCornerAndVolume(mSeekBar.getProgress(), MediaOutputSeekbar.scaleVolumeToProgress(currentVolume)); } else { @@ -284,6 +286,9 @@ public abstract class MediaOutputBaseAdapter extends } } } + if (mIsInitVolumeFirstTime) { + mIsInitVolumeFirstTime = false; + } mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index 5d2060d8043e..7b1ddd62ec6e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -139,12 +139,11 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { void updateResources(QSPanelController qsPanelController, QuickStatusBarHeaderController quickStatusBarHeaderController) { - int bottomPadding = getResources().getDimensionPixelSize(R.dimen.qs_panel_padding_bottom); mQSPanelContainer.setPaddingRelative( mQSPanelContainer.getPaddingStart(), QSUtils.getQsHeaderSystemIconsAreaHeight(mContext), mQSPanelContainer.getPaddingEnd(), - bottomPadding); + mQSPanelContainer.getPaddingBottom()); int horizontalMargins = getResources().getDimensionPixelSize(R.dimen.qs_horizontal_margin); int horizontalPadding = getResources().getDimensionPixelSize( diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 41724ef62683..324c01959084 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -362,11 +362,11 @@ public class QSPanel extends LinearLayout implements Tunable { protected void updatePadding() { final Resources res = mContext.getResources(); int paddingTop = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top); - // Bottom padding only when there's a new footer with its height. + int paddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom); setPaddingRelative(getPaddingStart(), paddingTop, getPaddingEnd(), - getPaddingBottom()); + paddingBottom); } void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 19eb168fddc0..89a15f65e98f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -142,7 +142,7 @@ public class ScreenshotController { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt index 478f7aa8ce09..c4947ca2dd90 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt @@ -56,4 +56,7 @@ class NotifPipelineFlags @Inject constructor( fun isSmartspaceDedupingEnabled(): Boolean = featureFlags.isEnabled(Flags.SMARTSPACE) && featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING) -}
\ No newline at end of file + + fun removeUnrankedNotifs(): Boolean = + featureFlags.isEnabled(Flags.REMOVE_UNRANKED_NOTIFICATIONS) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index 54912461da1e..6a01df61705d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -89,6 +89,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.In import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger; +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLoggerKt; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifEvent; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; @@ -110,6 +111,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Queue; +import java.util.Set; import java.util.concurrent.TimeUnit; import javax.inject.Inject; @@ -157,6 +159,8 @@ public class NotifCollection implements Dumpable, PipelineDumpable { private final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>(); private final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>(); + private Set<String> mNotificationsWithoutRankings = Collections.emptySet(); + private Queue<NotifEvent> mEventQueue = new ArrayDeque<>(); private boolean mAttached = false; @@ -560,6 +564,7 @@ public class NotifCollection implements Dumpable, PipelineDumpable { } private void applyRanking(@NonNull RankingMap rankingMap) { + ArrayMap<String, NotificationEntry> currentEntriesWithoutRankings = null; for (NotificationEntry entry : mNotificationSet.values()) { if (!isCanceled(entry)) { @@ -581,10 +586,27 @@ public class NotifCollection implements Dumpable, PipelineDumpable { } } } else { - mLogger.logRankingMissing(entry, rankingMap); + if (currentEntriesWithoutRankings == null) { + currentEntriesWithoutRankings = new ArrayMap<>(); + } + currentEntriesWithoutRankings.put(entry.getKey(), entry); } } } + NotifCollectionLoggerKt.maybeLogInconsistentRankings( + mLogger, + mNotificationsWithoutRankings, + currentEntriesWithoutRankings, + rankingMap + ); + mNotificationsWithoutRankings = currentEntriesWithoutRankings == null + ? Collections.emptySet() : currentEntriesWithoutRankings.keySet(); + if (currentEntriesWithoutRankings != null && mNotifPipelineFlags.removeUnrankedNotifs()) { + for (NotificationEntry entry : currentEntriesWithoutRankings.values()) { + entry.mCancellationReason = REASON_UNKNOWN; + tryRemoveNotification(entry); + } + } mEventQueue.add(new RankingAppliedEvent()); } @@ -836,6 +858,11 @@ public class NotifCollection implements Dumpable, PipelineDumpable { entries, true, "\t\t")); + + pw.println("\n\tmNotificationsWithoutRankings: " + mNotificationsWithoutRankings.size()); + for (String key : mNotificationsWithoutRankings) { + pw.println("\t * : " + key); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt index 46e01c33bae3..ebcac6b6afb3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt @@ -196,16 +196,32 @@ class NotifCollectionLogger @Inject constructor( }) } - fun logRankingMissing(entry: NotificationEntry, rankingMap: RankingMap) { + fun logMissingRankings( + newlyInconsistentEntries: List<NotificationEntry>, + totalInconsistent: Int, + rankingMap: RankingMap + ) { buffer.log(TAG, WARNING, { - str1 = entry.logKey + int1 = totalInconsistent + int2 = newlyInconsistentEntries.size + str1 = newlyInconsistentEntries.joinToString { it.logKey ?: "null" } + }, { + "Ranking update is missing ranking for $int1 entries ($int2 new): $str1" + }) + buffer.log(TAG, DEBUG, { + str1 = rankingMap.orderedKeys.map { logKey(it) ?: "null" }.toString() }, { - "Ranking update is missing ranking for $str1" + "Ranking map contents: $str1" + }) + } + + fun logRecoveredRankings(newlyConsistentKeys: List<String>) { + buffer.log(TAG, INFO, { + int1 = newlyConsistentKeys.size + str1 = newlyConsistentKeys.joinToString { logKey(it) ?: "null" } + }, { + "Ranking update now contains rankings for $int1 previously inconsistent entries: $str1" }) - buffer.log(TAG, DEBUG, {}, { "Ranking map contents:" }) - for (entry in rankingMap.orderedKeys) { - buffer.log(TAG, DEBUG, { str1 = logKey(entry) }, { " $str1" }) - } } fun logRemoteExceptionOnNotificationClear(entry: NotificationEntry, e: RemoteException) { @@ -346,4 +362,29 @@ class NotifCollectionLogger @Inject constructor( } } +fun maybeLogInconsistentRankings( + logger: NotifCollectionLogger, + oldKeysWithoutRankings: Set<String>, + newEntriesWithoutRankings: Map<String, NotificationEntry>?, + rankingMap: RankingMap +) { + if (oldKeysWithoutRankings.isEmpty() && newEntriesWithoutRankings == null) return + val newlyConsistent: List<String> = oldKeysWithoutRankings + .mapNotNull { key -> + key.takeIf { key !in (newEntriesWithoutRankings ?: emptyMap()) } + .takeIf { key in rankingMap.orderedKeys } + }.sorted() + if (newlyConsistent.isNotEmpty()) { + logger.logRecoveredRankings(newlyConsistent) + } + val newlyInconsistent: List<NotificationEntry> = newEntriesWithoutRankings + ?.mapNotNull { (key, entry) -> + entry.takeIf { key !in oldKeysWithoutRankings } + }?.sortedBy { it.key } ?: emptyList() + if (newlyInconsistent.isNotEmpty()) { + val totalInconsistent: Int = newEntriesWithoutRankings?.size ?: 0 + logger.logMissingRankings(newlyInconsistent, totalInconsistent, rankingMap) + } +} + private const val TAG = "NotifCollection" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 134f24e7e646..27aa4b38e0ba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -266,9 +266,14 @@ public class NotificationConversationInfo extends LinearLayout implements snooze.setOnClickListener(mOnSnoozeClick); */ - if (mAppBubble == BUBBLE_PREFERENCE_ALL) { - ((TextView) findViewById(R.id.default_summary)).setText(getResources().getString( + TextView defaultSummaryTextView = findViewById(R.id.default_summary); + if (mAppBubble == BUBBLE_PREFERENCE_ALL + && BubblesManager.areBubblesEnabled(mContext, mSbn.getUser())) { + defaultSummaryTextView.setText(getResources().getString( R.string.notification_channel_summary_default_with_bubbles, mAppName)); + } else { + defaultSummaryTextView.setText(getResources().getString( + R.string.notification_channel_summary_default)); } findViewById(R.id.priority).setOnClickListener(mOnFavoriteClick); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java index 414bc84fcd85..b43e9df79241 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -28,17 +28,13 @@ import static com.android.systemui.tuner.LockscreenFragment.LOCKSCREEN_RIGHT_UNL import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE; import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE; -import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.ColorStateList; @@ -46,13 +42,8 @@ import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; -import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; import android.os.RemoteException; import android.os.UserHandle; -import android.provider.MediaStore; -import android.service.media.CameraPrewarmService; import android.service.quickaccesswallet.GetWalletCardsError; import android.service.quickaccesswallet.GetWalletCardsResponse; import android.service.quickaccesswallet.QuickAccessWalletClient; @@ -172,20 +163,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL private KeyguardAffordanceHelper mAffordanceHelper; private FalsingManager mFalsingManager; private boolean mUserSetupComplete; - private boolean mPrewarmBound; - private Messenger mPrewarmMessenger; - private final ServiceConnection mPrewarmConnection = new ServiceConnection() { - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - mPrewarmMessenger = new Messenger(service); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - mPrewarmMessenger = null; - } - }; private boolean mLeftIsVoiceAssist; private Drawable mLeftAssistIcon; @@ -602,46 +579,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } } - public void bindCameraPrewarmService() { - Intent intent = getCameraIntent(); - ActivityInfo targetInfo = mActivityIntentHelper.getTargetActivityInfo(intent, - KeyguardUpdateMonitor.getCurrentUser(), true /* onlyDirectBootAware */); - if (targetInfo != null && targetInfo.metaData != null) { - String clazz = targetInfo.metaData.getString( - MediaStore.META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE); - if (clazz != null) { - Intent serviceIntent = new Intent(); - serviceIntent.setClassName(targetInfo.packageName, clazz); - serviceIntent.setAction(CameraPrewarmService.ACTION_PREWARM); - try { - if (getContext().bindServiceAsUser(serviceIntent, mPrewarmConnection, - Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, - new UserHandle(UserHandle.USER_CURRENT))) { - mPrewarmBound = true; - } - } catch (SecurityException e) { - Log.w(TAG, "Unable to bind to prewarm service package=" + targetInfo.packageName - + " class=" + clazz, e); - } - } - } - } - - public void unbindCameraPrewarmService(boolean launched) { - if (mPrewarmBound) { - if (mPrewarmMessenger != null && launched) { - try { - mPrewarmMessenger.send(Message.obtain(null /* handler */, - CameraPrewarmService.MSG_CAMERA_FIRED)); - } catch (RemoteException e) { - Log.w(TAG, "Error sending camera fired message", e); - } - } - mContext.unbindService(mPrewarmConnection); - mPrewarmBound = false; - } - } - public void launchCamera(String source) { final Intent intent = getCameraIntent(); intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source); @@ -651,8 +588,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL AsyncTask.execute(new Runnable() { @Override public void run() { - int result = ActivityManager.START_CANCELED; - // Normally an activity will set it's requested rotation // animation on its window. However when launching an activity // causes the orientation to change this is too late. In these cases @@ -666,7 +601,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL o.setRotationAnimationHint( WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS); try { - result = ActivityTaskManager.getService().startActivityAsUser( + ActivityTaskManager.getService().startActivityAsUser( null, getContext().getBasePackageName(), getContext().getAttributionTag(), intent, intent.resolveTypeIfNeeded(getContext().getContentResolver()), @@ -675,25 +610,12 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } catch (RemoteException e) { Log.w(TAG, "Unable to start camera activity", e); } - final boolean launched = isSuccessfulLaunch(result); - post(new Runnable() { - @Override - public void run() { - unbindCameraPrewarmService(launched); - } - }); } }); } else { // We need to delay starting the activity because ResolverActivity finishes itself if // launched behind lockscreen. - mActivityStarter.startActivity(intent, false /* dismissShade */, - new ActivityStarter.Callback() { - @Override - public void onActivityStarted(int resultCode) { - unbindCameraPrewarmService(isSuccessfulLaunch(resultCode)); - } - }); + mActivityStarter.startActivity(intent, false /* dismissShade */); } } @@ -705,12 +627,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL dozeTimeTick(); } - private static boolean isSuccessfulLaunch(int result) { - return result == ActivityManager.START_SUCCESS - || result == ActivityManager.START_DELIVERED_TO_TOP - || result == ActivityManager.START_TASK_TO_FRONT; - } - public void launchLeftAffordance() { if (mLeftIsVoiceAssist) { launchVoiceAssist(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 4e7624cf4994..b435055d1b16 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -4581,13 +4581,6 @@ public class NotificationPanelViewController extends PanelViewController { @Override public void onSwipingStarted(boolean rightIcon) { mFalsingCollector.onAffordanceSwipingStarted(rightIcon); - boolean - camera = - mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? !rightIcon - : rightIcon; - if (camera) { - mKeyguardBottomArea.bindCameraPrewarmService(); - } mView.requestDisallowInterceptTouchEvent(true); mOnlyAffordanceInThisMotion = true; mQsTracking = false; @@ -4596,7 +4589,6 @@ public class NotificationPanelViewController extends PanelViewController { @Override public void onSwipingAborted() { mFalsingCollector.onAffordanceSwipingAborted(); - mKeyguardBottomArea.unbindCameraPrewarmService(false /* launched */); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java index 6e331bc13294..fa701e704c21 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java @@ -89,6 +89,17 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh } public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock) { + // TODO(b/219008720): Remove those calls to Dependency.get by introducing a + // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set + // the content and attach listeners. + this(context, theme, dismissOnDeviceLock, Dependency.get(SystemUIDialogManager.class), + Dependency.get(SysUiState.class), Dependency.get(BroadcastDispatcher.class), + Dependency.get(DialogLaunchAnimator.class)); + } + + public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock, + SystemUIDialogManager dialogManager, SysUiState sysUiState, + BroadcastDispatcher broadcastDispatcher, DialogLaunchAnimator dialogLaunchAnimator) { super(context, theme); mContext = context; @@ -97,13 +108,10 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh attrs.setTitle(getClass().getSimpleName()); getWindow().setAttributes(attrs); - mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this) : null; - - // TODO(b/219008720): Remove those calls to Dependency.get by introducing a - // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set - // the content and attach listeners. - mDialogManager = Dependency.get(SystemUIDialogManager.class); - mSysUiState = Dependency.get(SysUiState.class); + mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this, broadcastDispatcher, + dialogLaunchAnimator) : null; + mDialogManager = dialogManager; + mSysUiState = sysUiState; } @Override @@ -322,7 +330,10 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh * @param dismissAction An action to run when the dialog is dismissed. */ public static void registerDismissListener(Dialog dialog, @Nullable Runnable dismissAction) { - DismissReceiver dismissReceiver = new DismissReceiver(dialog); + // TODO(b/219008720): Remove those calls to Dependency.get. + DismissReceiver dismissReceiver = new DismissReceiver(dialog, + Dependency.get(BroadcastDispatcher.class), + Dependency.get(DialogLaunchAnimator.class)); dialog.setOnDismissListener(d -> { dismissReceiver.unregister(); if (dismissAction != null) dismissAction.run(); @@ -404,11 +415,11 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh private final BroadcastDispatcher mBroadcastDispatcher; private final DialogLaunchAnimator mDialogLaunchAnimator; - DismissReceiver(Dialog dialog) { + DismissReceiver(Dialog dialog, BroadcastDispatcher broadcastDispatcher, + DialogLaunchAnimator dialogLaunchAnimator) { mDialog = dialog; - // TODO(b/219008720): Remove those calls to Dependency.get. - mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class); - mDialogLaunchAnimator = Dependency.get(DialogLaunchAnimator.class); + mBroadcastDispatcher = broadcastDispatcher; + mDialogLaunchAnimator = dialogLaunchAnimator; } void register() { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index cd2a43fc6a44..635ee9ea1a2f 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -43,6 +43,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.plugins.Clock; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -102,6 +103,8 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { private FrameLayout mLargeClockFrame; @Mock private SecureSettings mSecureSettings; + @Mock + private FeatureFlags mFeatureFlags; private final View mFakeSmartspaceView = new View(mContext); @@ -138,7 +141,8 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { mSecureSettings, mExecutor, mDumpManager, - mClockEventController + mClockEventController, + mFeatureFlags ); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index 1b9662ccae0c..f2ac0c7a7736 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -16,6 +16,8 @@ package com.android.keyguard; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE; import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT; import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT; @@ -46,7 +48,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; -import android.view.WindowInsetsController; import android.widget.FrameLayout; import androidx.test.filters.SmallTest; @@ -85,8 +86,6 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { public MockitoRule mRule = MockitoJUnit.rule(); @Mock - private WindowInsetsController mWindowInsetsController; - @Mock private KeyguardSecurityViewFlipper mSecurityViewFlipper; @Mock private GlobalSettings mGlobalSettings; @@ -110,7 +109,6 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { mSecurityViewFlipperLayoutParams = new FrameLayout.LayoutParams( MATCH_PARENT, MATCH_PARENT); - when(mSecurityViewFlipper.getWindowInsetsController()).thenReturn(mWindowInsetsController); when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams); mKeyguardSecurityContainer = new KeyguardSecurityContainer(getContext()); mKeyguardSecurityContainer.mSecurityViewFlipper = mSecurityViewFlipper; @@ -221,7 +219,7 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { mKeyguardSecurityContainer.getWidth() - 1f); verify(mGlobalSettings).putInt(ONE_HANDED_KEYGUARD_SIDE, ONE_HANDED_KEYGUARD_SIDE_RIGHT); - verify(mSecurityViewFlipper).setTranslationX( + assertSecurityTranslationX( mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth()); mKeyguardSecurityContainer.updatePositionByTouchX(1f); @@ -243,43 +241,43 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { } @Test - public void testUserSwitcherModeViewGravityLandscape() { + public void testUserSwitcherModeViewPositionLandscape() { // GIVEN one user has been setup and in landscape when(mUserSwitcherController.getUsers()).thenReturn(buildUserRecords(1)); - Configuration config = new Configuration(); - config.orientation = Configuration.ORIENTATION_LANDSCAPE; - when(getContext().getResources().getConfiguration()).thenReturn(config); + Configuration landscapeConfig = configuration(ORIENTATION_LANDSCAPE); + when(getContext().getResources().getConfiguration()).thenReturn(landscapeConfig); // WHEN UserSwitcherViewMode is initialized and config has changed setupUserSwitcher(); - reset(mSecurityViewFlipper); - when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams); - mKeyguardSecurityContainer.onConfigurationChanged(config); + mKeyguardSecurityContainer.onConfigurationChanged(landscapeConfig); // THEN views are oriented side by side - verify(mSecurityViewFlipper).setLayoutParams(mLayoutCaptor.capture()); - assertThat(mLayoutCaptor.getValue().gravity).isEqualTo(Gravity.RIGHT | Gravity.BOTTOM); + assertSecurityGravity(Gravity.LEFT | Gravity.BOTTOM); assertUserSwitcherGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL); + assertSecurityTranslationX( + mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth()); + assertUserSwitcherTranslationX(0f); + } @Test public void testUserSwitcherModeViewGravityPortrait() { // GIVEN one user has been setup and in landscape when(mUserSwitcherController.getUsers()).thenReturn(buildUserRecords(1)); - Configuration config = new Configuration(); - config.orientation = Configuration.ORIENTATION_PORTRAIT; - when(getContext().getResources().getConfiguration()).thenReturn(config); + Configuration portraitConfig = configuration(ORIENTATION_PORTRAIT); + when(getContext().getResources().getConfiguration()).thenReturn(portraitConfig); // WHEN UserSwitcherViewMode is initialized and config has changed setupUserSwitcher(); reset(mSecurityViewFlipper); when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams); - mKeyguardSecurityContainer.onConfigurationChanged(config); + mKeyguardSecurityContainer.onConfigurationChanged(portraitConfig); // THEN views are both centered horizontally - verify(mSecurityViewFlipper).setLayoutParams(mLayoutCaptor.capture()); - assertThat(mLayoutCaptor.getValue().gravity).isEqualTo(Gravity.CENTER_HORIZONTAL); + assertSecurityGravity(Gravity.CENTER_HORIZONTAL); assertUserSwitcherGravity(Gravity.CENTER_HORIZONTAL); + assertSecurityTranslationX(0); + assertUserSwitcherTranslationX(0); } @Test @@ -315,14 +313,16 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { setupUserSwitcher(); setViewWidth(VIEW_WIDTH); + // security is on the right side by default assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity( touchEventLeftSide())).isTrue(); assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity( touchEventRightSide())).isFalse(); - mKeyguardSecurityContainer.handleDoubleTap(touchEventLeftSide()); - // settings should be updated, we need to keep the mock updated as well + // move security to the left side when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_LEFT); + mKeyguardSecurityContainer.onConfigurationChanged(new Configuration()); + assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity( touchEventLeftSide())).isFalse(); assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity( @@ -331,23 +331,33 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { @Test public void testSecuritySwitchesSidesInLandscapeUserSwitcherMode() { - Configuration config = new Configuration(); - config.orientation = Configuration.ORIENTATION_LANDSCAPE; - when(getContext().getResources().getConfiguration()).thenReturn(config); + when(getContext().getResources().getConfiguration()) + .thenReturn(configuration(ORIENTATION_LANDSCAPE)); setupUserSwitcher(); - // check default position - assertSecurityGravity(Gravity.RIGHT | Gravity.BOTTOM); - assertUserSwitcherGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL); - reset(mSecurityViewFlipper); - when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams); - // change setting so configuration change triggers position change + // switch sides when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_LEFT); mKeyguardSecurityContainer.onConfigurationChanged(new Configuration()); - // check position after switching - assertSecurityGravity(Gravity.LEFT | Gravity.BOTTOM); - assertUserSwitcherGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL); + assertSecurityTranslationX(0); + assertUserSwitcherTranslationX( + mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth()); + } + + private Configuration configuration(@Configuration.Orientation int orientation) { + Configuration config = new Configuration(); + config.orientation = orientation; + return config; + } + + private void assertSecurityTranslationX(float translation) { + verify(mSecurityViewFlipper).setTranslationX(translation); + } + + private void assertUserSwitcherTranslationX(float translation) { + ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById( + R.id.keyguard_bouncer_user_switcher); + assertThat(userSwitcher.getTranslationX()).isEqualTo(translation); } private void assertUserSwitcherGravity(@Gravity.GravityFlags int gravity) { @@ -391,6 +401,9 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_RIGHT); mKeyguardSecurityContainer.initMode(KeyguardSecurityContainer.MODE_USER_SWITCHER, mGlobalSettings, mFalsingManager, mUserSwitcherController); + // reset mSecurityViewFlipper so setup doesn't influence test verifications + reset(mSecurityViewFlipper); + when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams); } private ArrayList<UserRecord> buildUserRecords(int count) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index a35efa995ddd..90609fa2772f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -210,7 +210,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { BOUNDS_POSITION_TOP, mAuthController, mStatusBarStateController, - mKeyguardUpdateMonitor)); + mKeyguardUpdateMonitor, + mExecutor)); mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings, mBroadcastDispatcher, mTunerService, mUserTracker, mDotViewController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt index d5df9fe0c2e8..c48cbb19b40a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt @@ -159,7 +159,7 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { @Test fun doesNotStartIfAnimationIsCancelled() { val runner = activityLaunchAnimator.createRunner(controller) - runner.onAnimationCancelled() + runner.onAnimationCancelled(false /* isKeyguardOccluded */) runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback) waitForIdleSync() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java index 11326e76b25e..59475cf0cb90 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -19,6 +19,7 @@ package com.android.systemui.media.dialog; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -51,6 +52,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { private static final String TEST_DEVICE_ID_1 = "test_device_id_1"; private static final String TEST_DEVICE_ID_2 = "test_device_id_2"; private static final String TEST_SESSION_NAME = "test_session_name"; + private static final int TEST_MAX_VOLUME = 20; + private static final int TEST_CURRENT_VOLUME = 10; // Mock private MediaOutputController mMediaOutputController = mock(MediaOutputController.class); @@ -64,12 +67,14 @@ public class MediaOutputAdapterTest extends SysuiTestCase { private MediaOutputAdapter mMediaOutputAdapter; private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder; private List<MediaDevice> mMediaDevices = new ArrayList<>(); + MediaOutputSeekbar mSpyMediaOutputSeekbar; @Before public void setUp() { mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController, mMediaOutputDialog); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); + mSpyMediaOutputSeekbar = spy(mViewHolder.mSeekBar); when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices); when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false); @@ -169,6 +174,16 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test + public void onBindViewHolder_initSeekbar_setsVolume() { + when(mMediaDevice1.getMaxVolume()).thenReturn(TEST_MAX_VOLUME); + when(mMediaDevice1.getCurrentVolume()).thenReturn(TEST_CURRENT_VOLUME); + mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); + + assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mViewHolder.mSeekBar.getVolume()).isEqualTo(TEST_CURRENT_VOLUME); + } + + @Test public void onBindViewHolder_bindNonActiveConnectedDevice_verifyView() { mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt index 489c8c86028e..bf237abba8fa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt @@ -57,6 +57,7 @@ class QSContainerImplTest : SysuiTestCase() { @Test fun testContainerBottomPadding() { + val originalPadding = qsPanelContainer.paddingBottom qsContainer.updateResources( qsPanelController, quickStatusBarHeaderController @@ -66,7 +67,7 @@ class QSContainerImplTest : SysuiTestCase() { anyInt(), anyInt(), anyInt(), - eq(mContext.resources.getDimensionPixelSize(R.dimen.footer_actions_height)) + eq(originalPadding) ) } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt index 60cfd7249919..b98be75a51c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt @@ -150,6 +150,14 @@ class QSPanelTest : SysuiTestCase() { assertThat(footer.isVisibleToUser).isTrue() } + @Test + fun testBottomPadding() { + val padding = 10 + context.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_bottom, padding) + qsPanel.updatePadding() + assertThat(qsPanel.paddingBottom).isEqualTo(padding) + } + private infix fun View.isLeftOf(other: View): Boolean { val rect = Rect() getBoundsOnScreen(rect) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt index 136a395196e1..131eac668af3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt @@ -110,6 +110,7 @@ class ClockRegistryTest : SysuiTestCase() { get() = settingValue set(value) { settingValue = value } } + registry.isEnabled = true verify(mockPluginManager) .addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java index 4507366e3073..ee7d55864931 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java @@ -85,6 +85,11 @@ public class NoManSimulator { mRankings.put(key, ranking); } + /** This is for testing error cases: b/216384850 */ + public Ranking removeRankingWithoutEvent(String key) { + return mRankings.remove(key); + } + private RankingMap buildRankingMap() { return new RankingMap(mRankings.values().toArray(new Ranking[0])); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java index 958d54230f1c..f286349971d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java @@ -1492,6 +1492,80 @@ public class NotifCollectionTest extends SysuiTestCase { } @Test + public void testMissingRankingWhenRemovalFeatureIsDisabled() { + // GIVEN a pipeline with one two notifications + when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(false); + String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key; + String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key; + NotificationEntry entry1 = mCollectionListener.getEntry(key1); + NotificationEntry entry2 = mCollectionListener.getEntry(key2); + clearInvocations(mCollectionListener); + + // GIVEN the message for removing key1 gets does not reach NotifCollection + Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1); + // WHEN the message for removing key2 arrives + mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL); + + // THEN only entry2 gets removed + verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL)); + verify(mCollectionListener).onEntryCleanUp(eq(entry2)); + verify(mCollectionListener).onRankingApplied(); + verifyNoMoreInteractions(mCollectionListener); + verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any()); + verify(mLogger, never()).logRecoveredRankings(any()); + clearInvocations(mCollectionListener, mLogger); + + // WHEN a ranking update includes key1 again + mNoMan.setRanking(key1, ranking1); + mNoMan.issueRankingUpdate(); + + // VERIFY that we do nothing but log the 'recovery' + verify(mCollectionListener).onRankingUpdate(any()); + verify(mCollectionListener).onRankingApplied(); + verifyNoMoreInteractions(mCollectionListener); + verify(mLogger, never()).logMissingRankings(any(), anyInt(), any()); + verify(mLogger).logRecoveredRankings(eq(List.of(key1))); + } + + @Test + public void testMissingRankingWhenRemovalFeatureIsEnabled() { + // GIVEN a pipeline with one two notifications + when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(true); + String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key; + String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key; + NotificationEntry entry1 = mCollectionListener.getEntry(key1); + NotificationEntry entry2 = mCollectionListener.getEntry(key2); + clearInvocations(mCollectionListener); + + // GIVEN the message for removing key1 gets does not reach NotifCollection + Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1); + // WHEN the message for removing key2 arrives + mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL); + + // THEN both entry1 and entry2 get removed + verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL)); + verify(mCollectionListener).onEntryRemoved(eq(entry1), eq(REASON_UNKNOWN)); + verify(mCollectionListener).onEntryCleanUp(eq(entry2)); + verify(mCollectionListener).onEntryCleanUp(eq(entry1)); + verify(mCollectionListener).onRankingApplied(); + verifyNoMoreInteractions(mCollectionListener); + verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any()); + verify(mLogger, never()).logRecoveredRankings(any()); + clearInvocations(mCollectionListener, mLogger); + + // WHEN a ranking update includes key1 again + mNoMan.setRanking(key1, ranking1); + mNoMan.issueRankingUpdate(); + + // VERIFY that we do nothing but log the 'recovery' + verify(mCollectionListener).onRankingUpdate(any()); + verify(mCollectionListener).onRankingApplied(); + verifyNoMoreInteractions(mCollectionListener); + verify(mLogger, never()).logMissingRankings(any(), anyInt(), any()); + verify(mLogger).logRecoveredRankings(eq(List.of(key1))); + } + + @Test public void testRegisterFutureDismissal() throws RemoteException { // GIVEN a pipeline with one notification NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt new file mode 100644 index 000000000000..6c07174bce65 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.collection.notifcollection + +import android.service.notification.NotificationListenerService.RankingMap +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class NotifCollectionLoggerTest : SysuiTestCase() { + private val logger: NotifCollectionLogger = mock() + private val entry1: NotificationEntry = NotificationEntryBuilder().setId(1).build() + private val entry2: NotificationEntry = NotificationEntryBuilder().setId(2).build() + + private fun mapOfEntries(vararg entries: NotificationEntry): Map<String, NotificationEntry> = + entries.associateBy { it.key } + + private fun rankingMapOf(vararg entries: NotificationEntry): RankingMap = + RankingMap(entries.map { it.ranking }.toTypedArray()) + + @Test + fun testMaybeLogInconsistentRankings_logsNewlyInconsistentRanking() { + val rankingMap = rankingMapOf(entry1) + maybeLogInconsistentRankings( + logger = logger, + oldKeysWithoutRankings = emptySet(), + newEntriesWithoutRankings = mapOfEntries(entry2), + rankingMap = rankingMap + ) + verify(logger).logMissingRankings( + newlyInconsistentEntries = eq(listOf(entry2)), + totalInconsistent = eq(1), + rankingMap = eq(rankingMap), + ) + verifyNoMoreInteractions(logger) + } + + @Test + fun testMaybeLogInconsistentRankings_doesNotLogAlreadyInconsistentRanking() { + maybeLogInconsistentRankings( + logger = logger, + oldKeysWithoutRankings = setOf(entry2.key), + newEntriesWithoutRankings = mapOfEntries(entry2), + rankingMap = rankingMapOf(entry1) + ) + verifyNoMoreInteractions(logger) + } + + @Test + fun testMaybeLogInconsistentRankings_logsWhenRankingIsAdded() { + maybeLogInconsistentRankings( + logger = logger, + oldKeysWithoutRankings = setOf(entry2.key), + newEntriesWithoutRankings = mapOfEntries(), + rankingMap = rankingMapOf(entry1, entry2) + ) + verify(logger).logRecoveredRankings( + newlyConsistentKeys = eq(listOf(entry2.key)), + ) + verifyNoMoreInteractions(logger) + } + + @Test + fun testMaybeLogInconsistentRankings_doesNotLogsWhenEntryIsRemoved() { + maybeLogInconsistentRankings( + logger = logger, + oldKeysWithoutRankings = setOf(entry2.key), + newEntriesWithoutRankings = mapOfEntries(), + rankingMap = rankingMapOf(entry1) + ) + verifyNoMoreInteractions(logger) + } +} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index ab9966f218c1..c7bd3a7c18aa 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -76,6 +76,7 @@ import android.os.Environment; import android.os.Handler; import android.os.Message; import android.os.Parcel; +import android.os.ParcelFileDescriptor; import android.os.PowerWhitelistManager; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -727,6 +728,19 @@ public class CompanionDeviceManagerService extends SystemService { } @Override + public void attachSystemDataTransport(String packageName, int userId, int associationId, + ParcelFileDescriptor fd) { + mSystemDataTransferProcessor.attachSystemDataTransport(packageName, userId, + associationId, fd); + } + + @Override + public void detachSystemDataTransport(String packageName, int userId, int associationId) { + mSystemDataTransferProcessor.detachSystemDataTransport(packageName, userId, + associationId); + } + + @Override public void notifyDeviceAppeared(int associationId) { if (DEBUG) Log.i(TAG, "notifyDevice_Appeared() id=" + associationId); diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java index 70745ba0b368..a839492dd7ac 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java @@ -37,16 +37,26 @@ import android.content.Intent; import android.os.Binder; import android.os.Bundle; import android.os.Handler; +import android.os.ParcelFileDescriptor; import android.os.ResultReceiver; import android.os.UserHandle; import android.permission.PermissionControllerManager; import android.util.Slog; +import android.util.SparseArray; +import com.android.internal.annotations.GuardedBy; import com.android.server.companion.AssociationStore; import com.android.server.companion.CompanionDeviceManagerService; import com.android.server.companion.PermissionsUtils; import com.android.server.companion.proto.CompanionMessage; +import libcore.io.IoUtils; +import libcore.io.Streams; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -77,6 +87,8 @@ public class SystemDataTransferProcessor { private final CompanionMessageProcessor mCompanionMessageProcessor; private final PermissionControllerManager mPermissionControllerManager; private final ExecutorService mExecutor; + @GuardedBy("mTransports") + private final SparseArray<Transport> mTransports = new SparseArray<>(); public SystemDataTransferProcessor(CompanionDeviceManagerService service, AssociationStore associationStore, @@ -92,10 +104,11 @@ public class SystemDataTransferProcessor { } /** - * Build a PendingIntent of permission sync user consent dialog + * Resolve the requested association, throwing if the caller doesn't have + * adequate permissions. */ - public PendingIntent buildPermissionTransferUserConsentIntent(String packageName, - @UserIdInt int userId, int associationId) { + private @NonNull AssociationInfo resolveAssociation(String packageName, int userId, + int associationId) { AssociationInfo association = mAssociationStore.getAssociationById(associationId); association = PermissionsUtils.sanitizeWithCallerChecks(mContext, association); if (association == null) { @@ -103,6 +116,15 @@ public class SystemDataTransferProcessor { + associationId + " is not associated with the app " + packageName + " for user " + userId); } + return association; + } + + /** + * Build a PendingIntent of permission sync user consent dialog + */ + public PendingIntent buildPermissionTransferUserConsentIntent(String packageName, + @UserIdInt int userId, int associationId) { + final AssociationInfo association = resolveAssociation(packageName, userId, associationId); // Check if the request's data type has been requested before. List<SystemDataTransferRequest> storedRequests = @@ -150,13 +172,7 @@ public class SystemDataTransferProcessor { Slog.i(LOG_TAG, "Start system data transfer for package [" + packageName + "] userId [" + userId + "] associationId [" + associationId + "]"); - AssociationInfo association = mAssociationStore.getAssociationById(associationId); - association = PermissionsUtils.sanitizeWithCallerChecks(mContext, association); - if (association == null) { - throw new DeviceNotAssociatedException("Association " - + associationId + " is not associated with the app " + packageName - + " for user " + userId); - } + final AssociationInfo association = resolveAssociation(packageName, userId, associationId); // Check if the request has been consented by the user. List<SystemDataTransferRequest> storedRequests = @@ -188,6 +204,35 @@ public class SystemDataTransferProcessor { } } + public void attachSystemDataTransport(String packageName, int userId, int associationId, + ParcelFileDescriptor fd) { + synchronized (mTransports) { + // TODO: restore once testing has evolved + // resolveAssociation(packageName, userId, associationId); + + if (mTransports.contains(associationId)) { + detachSystemDataTransport(packageName, userId, associationId); + } + + final Transport transport = new Transport(fd); + transport.start(); + mTransports.put(associationId, transport); + } + } + + public void detachSystemDataTransport(String packageName, int userId, int associationId) { + synchronized (mTransports) { + // TODO: restore once testing has evolved + // resolveAssociation(packageName, userId, associationId); + + final Transport transport = mTransports.get(associationId); + if (transport != null) { + mTransports.delete(associationId); + transport.stop(); + } + } + } + /** * Process a complete decrypted message reported by the companion app. */ @@ -242,4 +287,74 @@ public class SystemDataTransferProcessor { Slog.e(LOG_TAG, "Unknown result code:" + resultCode); } }; + + private class Transport { + private final InputStream mRemoteIn; + private final OutputStream mRemoteOut; + + private volatile boolean mStopped; + + public Transport(ParcelFileDescriptor fd) { + mRemoteIn = new ParcelFileDescriptor.AutoCloseInputStream(fd); + mRemoteOut = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + } + + public void start() { + new Thread(() -> { + try { + while (!mStopped) { + processNextCommand(); + } + } catch (IOException e) { + if (!mStopped) { + Slog.w(LOG_TAG, "Trouble during transport", e); + stop(); + } + } + }).start(); + } + + public void stop() { + mStopped = true; + + IoUtils.closeQuietly(mRemoteIn); + IoUtils.closeQuietly(mRemoteOut); + } + + private void processNextCommand() throws IOException { + Slog.d(LOG_TAG, "Waiting for next command..."); + + // Read message header + final byte[] headerBytes = new byte[8]; + Streams.readFully(mRemoteIn, headerBytes); + final ByteBuffer header = ByteBuffer.wrap(headerBytes); + final int command = header.getInt(); + final int length = header.getInt(); + + Slog.d(LOG_TAG, "Received command 0x" + Integer.toHexString(command) + + " length " + length); + switch (command) { + case 0x50490000: // PI(NG) version 0 + // Repeat back the given payload, within reason + final int target = Math.min(length, 1_000_000); + final byte[] payload = new byte[target]; + Streams.readFully(mRemoteIn, payload); + Streams.skipByReading(mRemoteIn, length - target); + + // Respond with PO(NG) version 0 + header.rewind(); + header.putInt(0x504F0000); + header.putInt(target); + mRemoteOut.write(header.array()); + mRemoteOut.write(payload); + break; + + default: + // Emit local warning, and skip message to + // handle next one + Slog.w(LOG_TAG, "Unknown command 0x" + Integer.toHexString(command)); + mRemoteIn.skip(length); + } + } + } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b31ce111ffba..d63fd53a383d 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -8790,8 +8790,12 @@ public class ActivityManagerService extends IActivityManager.Stub // otherwise the watchdog may be prevented from resetting the system. // Bail early if not published yet - if (ServiceManager.getService(Context.DROPBOX_SERVICE) == null) return; - final DropBoxManager dbox = mContext.getSystemService(DropBoxManager.class); + final DropBoxManager dbox; + try { + dbox = mContext.getSystemService(DropBoxManager.class); + } catch (Exception e) { + return; + } // Exit early if the dropbox isn't configured to accept this report type. final String dropboxTag = processClass(process) + "_" + eventType; diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 5c7af47682e4..d38cd8ef6181 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -3126,8 +3126,8 @@ public class AppOpsService extends IAppOpsService.Stub { if (callback == null) { return; } - final boolean mayWatchPackageName = - packageName != null && !filterAppAccessUnlocked(packageName); + final boolean mayWatchPackageName = packageName != null + && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(callingUid)); synchronized (this) { int switchOp = (op != AppOpsManager.OP_NONE) ? AppOpsManager.opToSwitch(op) : op; @@ -3331,7 +3331,8 @@ public class AppOpsService extends IAppOpsService.Stub { // When the caller is the system, it's possible that the packageName is the special // one (e.g., "root") which isn't actually existed. if (resolveUid(packageName) == uid - || (isPackageExisted(packageName) && !filterAppAccessUnlocked(packageName))) { + || (isPackageExisted(packageName) + && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(uid)))) { return AppOpsManager.MODE_ALLOWED; } return AppOpsManager.MODE_ERRORED; @@ -3350,10 +3351,10 @@ public class AppOpsService extends IAppOpsService.Stub { * * NOTE: This must not be called while synchronized on {@code this} to avoid dead locks */ - private boolean filterAppAccessUnlocked(String packageName) { + private boolean filterAppAccessUnlocked(String packageName, int userId) { final int callingUid = Binder.getCallingUid(); return LocalServices.getService(PackageManagerInternal.class) - .filterAppAccess(packageName, callingUid, UserHandle.getUserId(callingUid)); + .filterAppAccess(packageName, callingUid, userId); } @Override diff --git a/services/core/java/com/android/server/display/BrightnessThrottler.java b/services/core/java/com/android/server/display/BrightnessThrottler.java index 767b2d18a69a..eccee52f37aa 100644 --- a/services/core/java/com/android/server/display/BrightnessThrottler.java +++ b/services/core/java/com/android/server/display/BrightnessThrottler.java @@ -16,21 +16,31 @@ package com.android.server.display; +import android.annotation.NonNull; import android.content.Context; import android.hardware.display.BrightnessInfo; +import android.hardware.display.DisplayManager; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IThermalEventListener; import android.os.IThermalService; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.Temperature; +import android.provider.DeviceConfig; +import android.provider.DeviceConfigInterface; import android.util.Slog; -import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData; +import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.Executor; /** * This class monitors various conditions, such as skin temperature throttling status, and limits @@ -44,28 +54,54 @@ class BrightnessThrottler { private final Injector mInjector; private final Handler mHandler; - private BrightnessThrottlingData mThrottlingData; + // We need a separate handler for unit testing. These two handlers are the same throughout the + // non-test code. + private final Handler mDeviceConfigHandler; private final Runnable mThrottlingChangeCallback; private final SkinThermalStatusObserver mSkinThermalStatusObserver; + private final DeviceConfigListener mDeviceConfigListener; + private final DeviceConfigInterface mDeviceConfig; + private int mThrottlingStatus; + private BrightnessThrottlingData mThrottlingData; + private BrightnessThrottlingData mDdcThrottlingData; private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX; private @BrightnessInfo.BrightnessMaxReason int mBrightnessMaxReason = BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; + private String mUniqueDisplayId; + + // The most recent string that has been set from DeviceConfig + private String mBrightnessThrottlingDataString; + + // This is a collection of brightness throttling data that has been written as overrides from + // the DeviceConfig. This will always take priority over the display device config data. + private HashMap<String, BrightnessThrottlingData> mBrightnessThrottlingDataOverride = + new HashMap<>(1); BrightnessThrottler(Handler handler, BrightnessThrottlingData throttlingData, - Runnable throttlingChangeCallback) { - this(new Injector(), handler, throttlingData, throttlingChangeCallback); + Runnable throttlingChangeCallback, String uniqueDisplayId) { + this(new Injector(), handler, handler, throttlingData, throttlingChangeCallback, + uniqueDisplayId); } - BrightnessThrottler(Injector injector, Handler handler, BrightnessThrottlingData throttlingData, - Runnable throttlingChangeCallback) { + @VisibleForTesting + BrightnessThrottler(Injector injector, Handler handler, Handler deviceConfigHandler, + BrightnessThrottlingData throttlingData, Runnable throttlingChangeCallback, + String uniqueDisplayId) { mInjector = injector; + mHandler = handler; + mDeviceConfigHandler = deviceConfigHandler; mThrottlingData = throttlingData; + mDdcThrottlingData = throttlingData; mThrottlingChangeCallback = throttlingChangeCallback; mSkinThermalStatusObserver = new SkinThermalStatusObserver(mInjector, mHandler); - resetThrottlingData(mThrottlingData); + mUniqueDisplayId = uniqueDisplayId; + mDeviceConfig = injector.getDeviceConfig(); + mDeviceConfigListener = new DeviceConfigListener(); + + resetThrottlingData(mThrottlingData, mUniqueDisplayId); } boolean deviceSupportsThrottling() { @@ -86,7 +122,7 @@ class BrightnessThrottler { void stop() { mSkinThermalStatusObserver.stopObserving(); - + mDeviceConfig.removeOnPropertiesChangedListener(mDeviceConfigListener); // We're asked to stop throttling, so reset brightness restrictions. mBrightnessCap = PowerManager.BRIGHTNESS_MAX; mBrightnessMaxReason = BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; @@ -97,9 +133,19 @@ class BrightnessThrottler { mThrottlingStatus = THROTTLING_INVALID; } - void resetThrottlingData(BrightnessThrottlingData throttlingData) { + private void resetThrottlingData() { + resetThrottlingData(mDdcThrottlingData, mUniqueDisplayId); + } + + void resetThrottlingData(BrightnessThrottlingData throttlingData, String displayId) { stop(); - mThrottlingData = throttlingData; + + mUniqueDisplayId = displayId; + mDdcThrottlingData = throttlingData; + mDeviceConfigListener.startListening(); + reloadBrightnessThrottlingDataOverride(); + mThrottlingData = mBrightnessThrottlingDataOverride.getOrDefault(mUniqueDisplayId, + throttlingData); if (deviceSupportsThrottling()) { mSkinThermalStatusObserver.startObserving(); @@ -173,14 +219,148 @@ class BrightnessThrottler { private void dumpLocal(PrintWriter pw) { pw.println("BrightnessThrottler:"); pw.println(" mThrottlingData=" + mThrottlingData); + pw.println(" mDdcThrottlingData=" + mDdcThrottlingData); + pw.println(" mUniqueDisplayId=" + mUniqueDisplayId); pw.println(" mThrottlingStatus=" + mThrottlingStatus); pw.println(" mBrightnessCap=" + mBrightnessCap); pw.println(" mBrightnessMaxReason=" + BrightnessInfo.briMaxReasonToString(mBrightnessMaxReason)); + pw.println(" mBrightnessThrottlingDataOverride=" + mBrightnessThrottlingDataOverride); + pw.println(" mBrightnessThrottlingDataString=" + mBrightnessThrottlingDataString); mSkinThermalStatusObserver.dump(pw); } + private String getBrightnessThrottlingDataString() { + return mDeviceConfig.getString(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA, + /* defaultValue= */ null); + } + + private boolean parseAndSaveData(@NonNull String strArray, + @NonNull HashMap<String, BrightnessThrottlingData> tempBrightnessThrottlingData) { + boolean validConfig = true; + String[] items = strArray.split(","); + int i = 0; + + try { + String uniqueDisplayId = items[i++]; + + // number of throttling points + int noOfThrottlingPoints = Integer.parseInt(items[i++]); + List<ThrottlingLevel> throttlingLevels = new ArrayList<>(noOfThrottlingPoints); + + // throttling level and point + for (int j = 0; j < noOfThrottlingPoints; j++) { + String severity = items[i++]; + int status = parseThermalStatus(severity); + + float brightnessPoint = parseBrightness(items[i++]); + + throttlingLevels.add(new ThrottlingLevel(status, brightnessPoint)); + } + BrightnessThrottlingData toSave = + DisplayDeviceConfig.BrightnessThrottlingData.create(throttlingLevels); + tempBrightnessThrottlingData.put(uniqueDisplayId, toSave); + } catch (NumberFormatException | IndexOutOfBoundsException + | UnknownThermalStatusException e) { + validConfig = false; + Slog.e(TAG, "Throttling data is invalid array: '" + strArray + "'", e); + } + + if (i != items.length) { + validConfig = false; + } + + return validConfig; + } + + public void reloadBrightnessThrottlingDataOverride() { + HashMap<String, BrightnessThrottlingData> tempBrightnessThrottlingData = + new HashMap<>(1); + mBrightnessThrottlingDataString = getBrightnessThrottlingDataString(); + boolean validConfig = true; + mBrightnessThrottlingDataOverride.clear(); + if (mBrightnessThrottlingDataString != null) { + String[] throttlingDataSplits = mBrightnessThrottlingDataString.split(";"); + for (String s : throttlingDataSplits) { + if (!parseAndSaveData(s, tempBrightnessThrottlingData)) { + validConfig = false; + break; + } + } + + if (validConfig) { + mBrightnessThrottlingDataOverride.putAll(tempBrightnessThrottlingData); + tempBrightnessThrottlingData.clear(); + } + + } else { + Slog.w(TAG, "DeviceConfig BrightnessThrottlingData is null"); + } + } + + /** + * Listens to config data change and updates the brightness throttling data using + * DisplayManager#KEY_BRIGHTNESS_THROTTLING_DATA. + * The format should be a string similar to: "local:4619827677550801152,2,moderate,0.5,severe, + * 0.379518072;local:4619827677550801151,1,moderate,0.75" + * In this order: + * <displayId>,<no of throttling levels>,[<severity as string>,<brightness cap>] + * Where the latter part is repeated for each throttling level, and the entirety is repeated + * for each display, separated by a semicolon. + */ + public class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener { + public Executor mExecutor = new HandlerExecutor(mDeviceConfigHandler); + + public void startListening() { + mDeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + mExecutor, this); + } + + @Override + public void onPropertiesChanged(DeviceConfig.Properties properties) { + reloadBrightnessThrottlingDataOverride(); + resetThrottlingData(); + } + } + + private float parseBrightness(String intVal) throws NumberFormatException { + float value = Float.parseFloat(intVal); + if (value < PowerManager.BRIGHTNESS_MIN || value > PowerManager.BRIGHTNESS_MAX) { + throw new NumberFormatException("Brightness constraint value out of bounds."); + } + return value; + } + + @PowerManager.ThermalStatus private int parseThermalStatus(@NonNull String value) + throws UnknownThermalStatusException { + switch (value) { + case "none": + return PowerManager.THERMAL_STATUS_NONE; + case "light": + return PowerManager.THERMAL_STATUS_LIGHT; + case "moderate": + return PowerManager.THERMAL_STATUS_MODERATE; + case "severe": + return PowerManager.THERMAL_STATUS_SEVERE; + case "critical": + return PowerManager.THERMAL_STATUS_CRITICAL; + case "emergency": + return PowerManager.THERMAL_STATUS_EMERGENCY; + case "shutdown": + return PowerManager.THERMAL_STATUS_SHUTDOWN; + default: + throw new UnknownThermalStatusException("Invalid Thermal Status: " + value); + } + } + + private static class UnknownThermalStatusException extends Exception { + UnknownThermalStatusException(String message) { + super(message); + } + } + private final class SkinThermalStatusObserver extends IThermalEventListener.Stub { private final Injector mInjector; private final Handler mHandler; @@ -258,5 +438,10 @@ class BrightnessThrottler { return IThermalService.Stub.asInterface( ServiceManager.getService(Context.THERMAL_SERVICE)); } + + @NonNull + public DeviceConfigInterface getDeviceConfig() { + return DeviceConfigInterface.REAL; + } } } diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index a25ac210f9c8..2322280d8a9b 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -279,9 +279,13 @@ public class DisplayDeviceConfig { private HighBrightnessModeData mHbmData; private DensityMapping mDensityMapping; private String mLoadedFrom = null; + private Spline mSdrToHdrRatioSpline; + // Brightness Throttling data may be updated via the DeviceConfig. Here we store the original + // data, which comes from the ddc, and the current one, which may be the DeviceConfig + // overwritten value. private BrightnessThrottlingData mBrightnessThrottlingData; - private Spline mSdrToHdrRatioSpline; + private BrightnessThrottlingData mOriginalBrightnessThrottlingData; private DisplayDeviceConfig(Context context) { mContext = context; @@ -422,6 +426,10 @@ public class DisplayDeviceConfig { return config; } + void setBrightnessThrottlingData(BrightnessThrottlingData brightnessThrottlingData) { + mBrightnessThrottlingData = brightnessThrottlingData; + } + /** * Return the brightness mapping nits array. * @@ -637,6 +645,7 @@ public class DisplayDeviceConfig { + ", mHbmData=" + mHbmData + ", mSdrToHdrRatioSpline=" + mSdrToHdrRatioSpline + ", mBrightnessThrottlingData=" + mBrightnessThrottlingData + + ", mOriginalBrightnessThrottlingData=" + mOriginalBrightnessThrottlingData + ", mBrightnessRampFastDecrease=" + mBrightnessRampFastDecrease + ", mBrightnessRampFastIncrease=" + mBrightnessRampFastIncrease + ", mBrightnessRampSlowDecrease=" + mBrightnessRampSlowDecrease @@ -932,6 +941,7 @@ public class DisplayDeviceConfig { if (!badConfig) { mBrightnessThrottlingData = BrightnessThrottlingData.create(throttlingLevels); + mOriginalBrightnessThrottlingData = mBrightnessThrottlingData; } } @@ -1407,7 +1417,9 @@ public class DisplayDeviceConfig { /** * Container for brightness throttling data. */ - static class BrightnessThrottlingData { + public static class BrightnessThrottlingData { + public List<ThrottlingLevel> throttlingLevels; + static class ThrottlingLevel { public @PowerManager.ThermalStatus int thermalStatus; public float brightness; @@ -1421,9 +1433,25 @@ public class DisplayDeviceConfig { public String toString() { return "[" + thermalStatus + "," + brightness + "]"; } - } - public List<ThrottlingLevel> throttlingLevels; + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ThrottlingLevel)) { + return false; + } + ThrottlingLevel otherThrottlingLevel = (ThrottlingLevel) obj; + + return otherThrottlingLevel.thermalStatus == this.thermalStatus + && otherThrottlingLevel.brightness == this.brightness; + } + @Override + public int hashCode() { + int result = 1; + result = 31 * result + thermalStatus; + result = 31 * result + Float.hashCode(brightness); + return result; + } + } static public BrightnessThrottlingData create(List<ThrottlingLevel> throttlingLevels) { @@ -1482,12 +1510,30 @@ public class DisplayDeviceConfig { + "} "; } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof BrightnessThrottlingData)) { + return false; + } + + BrightnessThrottlingData otherBrightnessThrottlingData = (BrightnessThrottlingData) obj; + return throttlingLevels.equals(otherBrightnessThrottlingData.throttlingLevels); + } + + @Override + public int hashCode() { + return throttlingLevels.hashCode(); + } + private BrightnessThrottlingData(List<ThrottlingLevel> inLevels) { throttlingLevels = new ArrayList<>(inLevels.size()); for (ThrottlingLevel level : inLevels) { throttlingLevels.add(new ThrottlingLevel(level.thermalStatus, level.brightness)); } } - } } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index d05a902c6593..95c8fef12976 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -461,6 +461,18 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private boolean mIsRbcActive; + // Whether there's a callback to tell listeners the display has changed scheduled to run. When + // true it implies a wakelock is being held to guarantee the update happens before we collapse + // into suspend and so needs to be cleaned up if the thread is exiting. + // Should only be accessed on the Handler thread. + private boolean mOnStateChangedPending; + + // Count of proximity messages currently on this DPC's Handler. Used to keep track of how many + // suspend blocker acquisitions are pending when shutting down this DPC. + // Should only be accessed on the Handler thread. + private int mOnProximityPositiveMessages; + private int mOnProximityNegativeMessages; + // Animators. private ObjectAnimator mColorFadeOnAnimator; private ObjectAnimator mColorFadeOffAnimator; @@ -861,7 +873,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } }); mBrightnessThrottler.resetThrottlingData( - mDisplayDeviceConfig.getBrightnessThrottlingData()); + mDisplayDeviceConfig.getBrightnessThrottlingData(), mUniqueDisplayId); } private void sendUpdatePowerState() { @@ -1091,10 +1103,24 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mHbmController.stop(); mBrightnessThrottler.stop(); mHandler.removeCallbacksAndMessages(null); + + // Release any outstanding wakelocks we're still holding because of pending messages. if (mUnfinishedBusiness) { mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness); mUnfinishedBusiness = false; } + if (mOnStateChangedPending) { + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged); + mOnStateChangedPending = false; + } + for (int i = 0; i < mOnProximityPositiveMessages; i++) { + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive); + } + mOnProximityPositiveMessages = 0; + for (int i = 0; i < mOnProximityNegativeMessages; i++) { + mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative); + } + mOnProximityNegativeMessages = 0; final float brightness = mPowerState != null ? mPowerState.getScreenBrightness() @@ -1816,7 +1842,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call () -> { sendUpdatePowerStateLocked(); postBrightnessChangeRunnable(); - }); + }, mUniqueDisplayId); } private void blockScreenOn() { @@ -2248,8 +2274,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } private void sendOnStateChangedWithWakelock() { - mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdOnStateChanged); - mHandler.post(mOnStateChangedRunnable); + if (!mOnStateChangedPending) { + mOnStateChangedPending = true; + mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdOnStateChanged); + mHandler.post(mOnStateChangedRunnable); + } } private void logDisplayPolicyChanged(int newPolicy) { @@ -2408,6 +2437,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private final Runnable mOnStateChangedRunnable = new Runnable() { @Override public void run() { + mOnStateChangedPending = false; mCallbacks.onStateChanged(); mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged); } @@ -2416,17 +2446,20 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private void sendOnProximityPositiveWithWakelock() { mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxPositive); mHandler.post(mOnProximityPositiveRunnable); + mOnProximityPositiveMessages++; } private final Runnable mOnProximityPositiveRunnable = new Runnable() { @Override public void run() { + mOnProximityPositiveMessages--; mCallbacks.onProximityPositive(); mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive); } }; private void sendOnProximityNegativeWithWakelock() { + mOnProximityNegativeMessages++; mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxNegative); mHandler.post(mOnProximityNegativeRunnable); } @@ -2434,6 +2467,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private final Runnable mOnProximityNegativeRunnable = new Runnable() { @Override public void run() { + mOnProximityNegativeMessages--; mCallbacks.onProximityNegative(); mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative); } @@ -2533,6 +2567,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call pw.println(" mReportedToPolicy=" + reportedToPolicyToString(mReportedScreenStateToPolicy)); pw.println(" mIsRbcActive=" + mIsRbcActive); + pw.println(" mOnStateChangePending=" + mOnStateChangedPending); + pw.println(" mOnProximityPositiveMessages=" + mOnProximityPositiveMessages); + pw.println(" mOnProximityNegativeMessages=" + mOnProximityNegativeMessages); if (mScreenBrightnessRampAnimator != null) { pw.println(" mScreenBrightnessRampAnimator.isAnimating()=" diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java index 8de150ac4124..223b8c181fea 100644 --- a/services/core/java/com/android/server/display/color/ColorDisplayService.java +++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java @@ -956,6 +956,8 @@ public final class ColorDisplayService extends SystemService { R.array.config_availableColorModes); if (availableColorModes.length > 0) { colorMode = availableColorModes[0]; + } else { + colorMode = NOT_SET; } } } diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java index 0186fe5c357c..4ffad91bbfad 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java @@ -149,10 +149,11 @@ final class IInputMethodInvoker { @AnyThread void startInput(IBinder startInputToken, IRemoteInputConnection inputConnection, - EditorInfo attribute, boolean restarting, @InputMethodNavButtonFlags int navButtonFlags, + EditorInfo editorInfo, boolean restarting, + @InputMethodNavButtonFlags int navButtonFlags, @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { try { - mTarget.startInput(startInputToken, inputConnection, attribute, restarting, + mTarget.startInput(startInputToken, inputConnection, editorInfo, restarting, navButtonFlags, imeDispatcher); } catch (RemoteException e) { logRemoteException(e); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java new file mode 100644 index 000000000000..68753ab909b3 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import static com.android.server.inputmethod.SubtypeUtils.SUBTYPE_MODE_ANY; +import static com.android.server.inputmethod.SubtypeUtils.SUBTYPE_MODE_KEYBOARD; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; + +/** + * This class provides utility methods to generate or filter {@link InputMethodInfo} for + * {@link InputMethodManagerService}. + * + * <p>This class is intentionally package-private. Utility methods here are tightly coupled with + * implementation details in {@link InputMethodManagerService}. Hence this class is not suitable + * for other components to directly use.</p> + */ +final class InputMethodInfoUtils { + private static final String TAG = "InputMethodInfoUtils"; + + /** + * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs + * that are mainly used until the system becomes ready. Note that {@link Locale} in this array + * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH} + * doesn't automatically match {@code Locale("en", "IN")}. + */ + private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = { + Locale.ENGLISH, // "en" + Locale.US, // "en_US" + Locale.UK, // "en_GB" + }; + private static final Locale ENGLISH_LOCALE = new Locale("en"); + + private static final class InputMethodListBuilder { + // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration + // order can have non-trivial effect in the call sites. + @NonNull + private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>(); + + InputMethodListBuilder fillImes(ArrayList<InputMethodInfo> imis, Context context, + boolean checkDefaultAttribute, @Nullable Locale locale, boolean checkCountry, + String requiredSubtypeMode) { + for (int i = 0; i < imis.size(); ++i) { + final InputMethodInfo imi = imis.get(i); + if (isSystemImeThatHasSubtypeOf(imi, context, + checkDefaultAttribute, locale, checkCountry, requiredSubtypeMode)) { + mInputMethodSet.add(imi); + } + } + return this; + } + + // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be + // documented more clearly. + InputMethodListBuilder fillAuxiliaryImes(ArrayList<InputMethodInfo> imis, Context context) { + // If one or more auxiliary input methods are available, OK to stop populating the list. + for (final InputMethodInfo imi : mInputMethodSet) { + if (imi.isAuxiliaryIme()) { + return this; + } + } + boolean added = false; + for (int i = 0; i < imis.size(); ++i) { + final InputMethodInfo imi = imis.get(i); + if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, + true /* checkDefaultAttribute */)) { + mInputMethodSet.add(imi); + added = true; + } + } + if (added) { + return this; + } + for (int i = 0; i < imis.size(); ++i) { + final InputMethodInfo imi = imis.get(i); + if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, + false /* checkDefaultAttribute */)) { + mInputMethodSet.add(imi); + } + } + return this; + + } + + public boolean isEmpty() { + return mInputMethodSet.isEmpty(); + } + + @NonNull + public ArrayList<InputMethodInfo> build() { + return new ArrayList<>(mInputMethodSet); + } + } + + private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale( + ArrayList<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale, + @Nullable Locale fallbackLocale) { + // Once the system becomes ready, we pick up at least one keyboard in the following order. + // Secondary users fall into this category in general. + // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true + // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false + // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true + // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false + // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true + // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false + // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. + + final InputMethodListBuilder builder = new InputMethodListBuilder(); + builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); + if (!builder.isEmpty()) { + return builder; + } + Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray()) + + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale); + return builder; + } + + static ArrayList<InputMethodInfo> getDefaultEnabledImes( + Context context, ArrayList<InputMethodInfo> imis, boolean onlyMinimum) { + final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context); + // We will primarily rely on the system locale, but also keep relying on the fallback locale + // as a last resort. + // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs), + // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic" + // subtype) + final Locale systemLocale = LocaleUtils.getSystemLocaleFromContext(context); + final InputMethodListBuilder builder = + getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale); + if (!onlyMinimum) { + builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, + true /* checkCountry */, SUBTYPE_MODE_ANY) + .fillAuxiliaryImes(imis, context); + } + return builder.build(); + } + + static ArrayList<InputMethodInfo> getDefaultEnabledImes( + Context context, ArrayList<InputMethodInfo> imis) { + return getDefaultEnabledImes(context, imis, false /* onlyMinimum */); + } + + /** + * Chooses an eligible system voice IME from the given IMEs. + * + * @param methodMap Map from the IME ID to {@link InputMethodInfo}. + * @param systemSpeechRecognizerPackageName System speech recognizer configured by the system + * config. + * @param currentDefaultVoiceImeId IME ID currently set to + * {@link Settings.Secure#DEFAULT_VOICE_INPUT_METHOD} + * @return {@link InputMethodInfo} that is found in {@code methodMap} and most suitable for + * the system voice IME. + */ + @Nullable + static InputMethodInfo chooseSystemVoiceIme( + @NonNull ArrayMap<String, InputMethodInfo> methodMap, + @Nullable String systemSpeechRecognizerPackageName, + @Nullable String currentDefaultVoiceImeId) { + if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) { + return null; + } + final InputMethodInfo defaultVoiceIme = methodMap.get(currentDefaultVoiceImeId); + // If the config matches the package of the setting, use the current one. + if (defaultVoiceIme != null && defaultVoiceIme.isSystem() + && defaultVoiceIme.getPackageName().equals(systemSpeechRecognizerPackageName)) { + return defaultVoiceIme; + } + InputMethodInfo firstMatchingIme = null; + final int methodCount = methodMap.size(); + for (int i = 0; i < methodCount; ++i) { + final InputMethodInfo imi = methodMap.valueAt(i); + if (!imi.isSystem()) { + continue; + } + if (!TextUtils.equals(imi.getPackageName(), systemSpeechRecognizerPackageName)) { + continue; + } + if (firstMatchingIme != null) { + Slog.e(TAG, "At most one InputMethodService can be published in " + + "systemSpeechRecognizer: " + systemSpeechRecognizerPackageName + + ". Ignoring all of them."); + return null; + } + firstMatchingIme = imi; + } + return firstMatchingIme; + } + + static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) { + if (enabledImes == null || enabledImes.isEmpty()) { + return null; + } + // We'd prefer to fall back on a system IME, since that is safer. + int i = enabledImes.size(); + int firstFoundSystemIme = -1; + while (i > 0) { + i--; + final InputMethodInfo imi = enabledImes.get(i); + if (imi.isAuxiliaryIme()) { + continue; + } + if (imi.isSystem() && SubtypeUtils.containsSubtypeOf(imi, ENGLISH_LOCALE, + false /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { + return imi; + } + if (firstFoundSystemIme < 0 && imi.isSystem()) { + firstFoundSystemIme = i; + } + } + return enabledImes.get(Math.max(firstFoundSystemIme, 0)); + } + + private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(InputMethodInfo imi, + Context context, boolean checkDefaultAttribute) { + if (!imi.isSystem()) { + return false; + } + if (checkDefaultAttribute && !imi.isDefault(context)) { + return false; + } + if (!imi.isAuxiliaryIme()) { + return false; + } + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype s = imi.getSubtypeAt(i); + if (s.overridesImplicitlyEnabledSubtype()) { + return true; + } + } + return false; + } + + @Nullable + private static Locale getFallbackLocaleForDefaultIme(ArrayList<InputMethodInfo> imis, + Context context) { + // At first, find the fallback locale from the IMEs that are declared as "default" in the + // current locale. Note that IME developers can declare an IME as "default" only for + // some particular locales but "not default" for other locales. + for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { + for (int i = 0; i < imis.size(); ++i) { + if (isSystemImeThatHasSubtypeOf(imis.get(i), context, + true /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SubtypeUtils.SUBTYPE_MODE_KEYBOARD)) { + return fallbackLocale; + } + } + } + // If no fallback locale is found in the above condition, find fallback locales regardless + // of the "default" attribute as a last resort. + for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { + for (int i = 0; i < imis.size(); ++i) { + if (isSystemImeThatHasSubtypeOf(imis.get(i), context, + false /* checkDefaultAttribute */, fallbackLocale, + true /* checkCountry */, SubtypeUtils.SUBTYPE_MODE_KEYBOARD)) { + return fallbackLocale; + } + } + } + Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray())); + return null; + } + + private static boolean isSystemImeThatHasSubtypeOf(InputMethodInfo imi, Context context, + boolean checkDefaultAttribute, @Nullable Locale requiredLocale, boolean checkCountry, + String requiredSubtypeMode) { + if (!imi.isSystem()) { + return false; + } + if (checkDefaultAttribute && !imi.isDefault(context)) { + return false; + } + if (!SubtypeUtils.containsSubtypeOf(imi, requiredLocale, checkCountry, + requiredSubtypeMode)) { + return false; + } + return true; + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 7c4803b2b477..5a8190a833e3 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -596,9 +596,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Nullable IRemoteAccessibilityInputConnection mCurRemoteAccessibilityInputConnection; /** - * The attributes last provided by the current client. + * The {@link EditorInfo} last provided by the current client. */ - EditorInfo mCurAttribute; + EditorInfo mCurEditorInfo; /** * A special {@link Matrix} to convert virtual screen coordinates to the IME target display @@ -1787,7 +1787,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (selectedMethodId != null && !mMethodMap.get(selectedMethodId).isSystem()) { return; } - final List<InputMethodInfo> suitableImes = InputMethodUtils.getDefaultEnabledImes( + final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes( context, mSettings.getEnabledInputMethodListLocked()); if (suitableImes.isEmpty()) { Slog.i(TAG, "No default found"); @@ -2393,7 +2393,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub getCurTokenLocked(), mCurTokenDisplayId, getCurIdLocked(), startInputReason, restarting, UserHandle.getUserId(mCurClient.uid), mCurClient.selfReportedDisplayId, - mCurFocusedWindow, mCurAttribute, mCurFocusedWindowSoftInputMode, + mCurFocusedWindow, mCurEditorInfo, mCurFocusedWindowSoftInputMode, getSequenceNumberLocked()); mImeTargetWindowMap.put(startInputToken, mCurFocusedWindow); mStartInputHistory.addEntry(info); @@ -2413,7 +2413,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final int navButtonFlags = getInputMethodNavButtonFlagsLocked(); final SessionState session = mCurClient.curSession; setEnabledSessionLocked(session); - session.method.startInput(startInputToken, mCurInputConnection, mCurAttribute, restarting, + session.method.startInput(startInputToken, mCurInputConnection, mCurEditorInfo, restarting, navButtonFlags, mCurImeDispatcher); if (mShowRequested) { if (DEBUG) Slog.v(TAG, "Attach new input asks to show input"); @@ -2479,7 +2479,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final Binder startInputToken = new Binder(); setEnabledSessionForAccessibilityLocked(mCurClient.mAccessibilitySessions); AccessibilityManagerInternal.get().startInput(mCurRemoteAccessibilityInputConnection, - mCurAttribute, !initial /* restarting */); + mCurEditorInfo, !initial /* restarting */); } if (accessibilitySession != null) { @@ -2522,7 +2522,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IRemoteInputConnection inputConnection, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, - @NonNull EditorInfo attribute, @StartInputFlags int startInputFlags, + @NonNull EditorInfo editorInfo, @StartInputFlags int startInputFlags, @StartInputReason int startInputReason, int unverifiedTargetSdkVersion, @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { @@ -2541,9 +2541,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (!InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager, cs.uid, - attribute.packageName)) { + editorInfo.packageName)) { Slog.e(TAG, "Rejecting this client as it reported an invalid package name." - + " uid=" + cs.uid + " package=" + attribute.packageName); + + " uid=" + cs.uid + " package=" + editorInfo.packageName); return InputBindResult.INVALID_PACKAGE_NAME; } @@ -2573,7 +2573,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mCurVirtualDisplayToScreenMatrix = getVirtualDisplayToScreenMatrixLocked(cs.selfReportedDisplayId, mDisplayIdToShowIme); - mCurAttribute = attribute; + mCurEditorInfo = editorInfo; // If configured, we want to avoid starting up the IME if it is not supposed to be showing if (shouldPreventImeStartupLocked(selectedMethodId, startInputFlags, @@ -3578,12 +3578,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub public InputBindResult startInputOrWindowGainedFocus( @StartInputReason int startInputReason, IInputMethodClient client, IBinder windowToken, @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode, - int windowFlags, @Nullable EditorInfo attribute, IRemoteInputConnection inputConnection, + int windowFlags, @Nullable EditorInfo editorInfo, + IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { return startInputOrWindowGainedFocusInternal(startInputReason, client, windowToken, - startInputFlags, softInputMode, windowFlags, attribute, inputConnection, + startInputFlags, softInputMode, windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, imeDispatcher); } @@ -3592,7 +3593,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private InputBindResult startInputOrWindowGainedFocusInternal( @StartInputReason int startInputReason, IInputMethodClient client, IBinder windowToken, @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode, - int windowFlags, @Nullable EditorInfo attribute, + int windowFlags, @Nullable EditorInfo editorInfo, @Nullable IRemoteInputConnection inputConnection, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @@ -3608,13 +3609,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub "InputMethodManagerService#startInputOrWindowGainedFocus"); final int callingUserId = UserHandle.getCallingUserId(); final int userId; - if (attribute != null && attribute.targetInputMethodUser != null - && attribute.targetInputMethodUser.getIdentifier() != callingUserId) { + if (editorInfo != null && editorInfo.targetInputMethodUser != null + && editorInfo.targetInputMethodUser.getIdentifier() != callingUserId) { mContext.enforceCallingPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, "Using EditorInfo.targetInputMethodUser requires" + " INTERACT_ACROSS_USERS_FULL."); - userId = attribute.targetInputMethodUser.getIdentifier(); + userId = editorInfo.targetInputMethodUser.getIdentifier(); if (!mUserManagerInternal.isUserRunning(userId)) { // There is a chance that we hit here because of race condition. Let's just // return an error code instead of crashing the caller process, which at @@ -3632,7 +3633,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub try { result = startInputOrWindowGainedFocusInternalLocked(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, - attribute, inputConnection, remoteAccessibilityInputConnection, + editorInfo, inputConnection, remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId, imeDispatcher); } finally { Binder.restoreCallingIdentity(ident); @@ -3643,7 +3644,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Slog.wtf(TAG, "InputBindResult is @NonNull. startInputReason=" + InputMethodDebug.startInputReasonToString(startInputReason) + " windowFlags=#" + Integer.toHexString(windowFlags) - + " editorInfo=" + attribute); + + " editorInfo=" + editorInfo); return InputBindResult.NULL; } @@ -3658,7 +3659,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private InputBindResult startInputOrWindowGainedFocusInternalLocked( @StartInputReason int startInputReason, IInputMethodClient client, @NonNull IBinder windowToken, @StartInputFlags int startInputFlags, - @SoftInputModeFlags int softInputMode, int windowFlags, EditorInfo attribute, + @SoftInputModeFlags int softInputMode, int windowFlags, EditorInfo editorInfo, IRemoteInputConnection inputContext, @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, @@ -3668,7 +3669,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + InputMethodDebug.startInputReasonToString(startInputReason) + " client=" + client.asBinder() + " inputContext=" + inputContext - + " attribute=" + attribute + + " editorInfo=" + editorInfo + " startInputFlags=" + InputMethodDebug.startInputFlagsToString(startInputFlags) + " softInputMode=" + InputMethodDebug.softInputModeToString(softInputMode) @@ -3751,13 +3752,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (sameWindowFocused && isTextEditor) { if (DEBUG) { Slog.w(TAG, "Window already focused, ignoring focus gain of: " + client - + " attribute=" + attribute + ", token = " + windowToken + + " editorInfo=" + editorInfo + ", token = " + windowToken + ", startInputReason=" + InputMethodDebug.startInputReasonToString(startInputReason)); } - if (attribute != null) { + if (editorInfo != null) { return startInputUncheckedLocked(cs, inputContext, - remoteAccessibilityInputConnection, attribute, startInputFlags, + remoteAccessibilityInputConnection, editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion, imeDispatcher); } return new InputBindResult( @@ -3796,10 +3797,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // state is ALWAYS_HIDDEN or STATE_HIDDEN with forward navigation). // Because the app might leverage these flags to hide soft-keyboard with showing their own // UI for input. - if (isTextEditor && attribute != null + if (isTextEditor && editorInfo != null && shouldRestoreImeVisibility(windowToken, softInputMode)) { res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, - attribute, startInputFlags, startInputReason, unverifiedTargetSdkVersion, + editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion, imeDispatcher); showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null, SoftInputShowHideReason.SHOW_RESTORE_IME_VISIBILITY); @@ -3837,9 +3838,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // by the IME) or if running on a large screen where there // is more room for the target window + IME. if (DEBUG) Slog.v(TAG, "Unspecified window will show input"); - if (attribute != null) { + if (editorInfo != null) { res = startInputUncheckedLocked(cs, inputContext, - remoteAccessibilityInputConnection, attribute, startInputFlags, + remoteAccessibilityInputConnection, editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion, imeDispatcher); didStart = true; @@ -3870,9 +3871,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (DEBUG) Slog.v(TAG, "Window asks to show input going forward"); if (isSoftInputModeStateVisibleAllowed( unverifiedTargetSdkVersion, startInputFlags)) { - if (attribute != null) { + if (editorInfo != null) { res = startInputUncheckedLocked(cs, inputContext, - remoteAccessibilityInputConnection, attribute, startInputFlags, + remoteAccessibilityInputConnection, editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion, imeDispatcher); didStart = true; @@ -3891,9 +3892,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (isSoftInputModeStateVisibleAllowed( unverifiedTargetSdkVersion, startInputFlags)) { if (!sameWindowFocused) { - if (attribute != null) { + if (editorInfo != null) { res = startInputUncheckedLocked(cs, inputContext, - remoteAccessibilityInputConnection, attribute, startInputFlags, + remoteAccessibilityInputConnection, editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion, imeDispatcher); didStart = true; @@ -3910,7 +3911,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (!didStart) { - if (attribute != null) { + if (editorInfo != null) { if (sameWindowFocused) { // On previous platforms, when Dialogs re-gained focus, the Activity behind // would briefly gain focus first, and dismiss the IME. @@ -3924,7 +3925,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } res = startInputUncheckedLocked(cs, inputContext, - remoteAccessibilityInputConnection, attribute, startInputFlags, + remoteAccessibilityInputConnection, editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion, imeDispatcher); } else { @@ -4048,7 +4049,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (subtype != null) { setInputMethodWithSubtypeIdLocked(token, id, - InputMethodUtils.getSubtypeIdFromHashCode(mMethodMap.get(id), + SubtypeUtils.getSubtypeIdFromHashCode(mMethodMap.get(id), subtype.hashCode())); } else { setInputMethod(token, id); @@ -4092,7 +4093,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // defined, there is no need to switch to the last IME. if (!imiIdIsSame || lastSubtypeHash != currentSubtypeHash) { targetLastImiId = lastIme.first; - subtypeId = InputMethodUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash); + subtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash); } } @@ -4111,13 +4112,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final InputMethodInfo imi = enabled.get(i); if (imi.getSubtypeCount() > 0 && imi.isSystem()) { InputMethodSubtype keyboardSubtype = - InputMethodUtils.findLastResortApplicableSubtypeLocked(mRes, - InputMethodUtils.getSubtypes(imi), - InputMethodUtils.SUBTYPE_MODE_KEYBOARD, locale, true); + SubtypeUtils.findLastResortApplicableSubtypeLocked(mRes, + SubtypeUtils.getSubtypes(imi), + SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); if (keyboardSubtype != null) { targetLastImiId = imi.getId(); - subtypeId = InputMethodUtils.getSubtypeIdFromHashCode( - imi, keyboardSubtype.hashCode()); + subtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi, + keyboardSubtype.hashCode()); if(keyboardSubtype.getLocale().equals(locale)) { break; } @@ -4187,8 +4188,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (lastImi == null) return null; try { final int lastSubtypeHash = Integer.parseInt(lastIme.second); - final int lastSubtypeId = - InputMethodUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash); + final int lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi, + lastSubtypeHash); if (lastSubtypeId < 0 || lastSubtypeId >= lastImi.getSubtypeCount()) { return null; } @@ -4486,8 +4487,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mWindowManagerInternal.getWindowName(mLastImeTargetWindow)); proto.write(CUR_FOCUSED_WINDOW_SOFT_INPUT_MODE, InputMethodDebug.softInputModeToString(mCurFocusedWindowSoftInputMode)); - if (mCurAttribute != null) { - mCurAttribute.dumpDebug(proto, CUR_ATTRIBUTE); + if (mCurEditorInfo != null) { + mCurEditorInfo.dumpDebug(proto, CUR_ATTRIBUTE); } proto.write(CUR_ID, getCurIdLocked()); proto.write(SHOW_REQUESTED, mShowRequested); @@ -4602,7 +4603,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mWindowManagerInternal.onToggleImeRequested( show, mCurFocusedWindow, requestToken, mCurTokenDisplayId); mSoftInputShowHideHistory.addEntry(new SoftInputShowHideHistory.Entry( - mCurFocusedWindowClient, mCurAttribute, info.focusedWindowName, + mCurFocusedWindowClient, mCurEditorInfo, info.focusedWindowName, mCurFocusedWindowSoftInputMode, reason, mInFullscreenMode, info.requestWindowName, info.imeControlTargetName, info.imeLayerTargetName)); } @@ -4867,7 +4868,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean chooseNewDefaultIMELocked() { - final InputMethodInfo imi = InputMethodUtils.getMostApplicableDefaultIME( + final InputMethodInfo imi = InputMethodInfoUtils.getMostApplicableDefaultIME( mSettings.getEnabledInputMethodListLocked()); if (imi != null) { if (DEBUG) { @@ -5010,7 +5011,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (resetDefaultEnabledIme || reenableMinimumNonAuxSystemImes) { final ArrayList<InputMethodInfo> defaultEnabledIme = - InputMethodUtils.getDefaultEnabledImes(mContext, mMethodList, + InputMethodInfoUtils.getDefaultEnabledImes(mContext, mMethodList, reenableMinimumNonAuxSystemImes); final int N = defaultEnabledIme.size(); for (int i = 0; i < N; ++i) { @@ -5066,7 +5067,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final String systemSpeechRecognizer = mContext.getString(com.android.internal.R.string.config_systemSpeechRecognizer); final String currentDefaultVoiceImeId = mSettings.getDefaultVoiceInputMethod(); - final InputMethodInfo newSystemVoiceIme = InputMethodUtils.chooseSystemVoiceIme( + final InputMethodInfo newSystemVoiceIme = InputMethodInfoUtils.chooseSystemVoiceIme( mMethodMap, systemSpeechRecognizer, currentDefaultVoiceImeId); if (newSystemVoiceIme == null) { if (DEBUG) { @@ -5192,8 +5193,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub String subtypeHashCode = mSettings.getLastSubtypeForInputMethodLocked(newDefaultIme); if (subtypeHashCode != null) { try { - lastSubtypeId = InputMethodUtils.getSubtypeIdFromHashCode( - imi, Integer.parseInt(subtypeHashCode)); + lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi, + Integer.parseInt(subtypeHashCode)); } catch (NumberFormatException e) { Slog.w(TAG, "HashCode for subtype looks broken: " + subtypeHashCode, e); } @@ -5228,7 +5229,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return null; } if (!subtypeIsSelected || mCurrentSubtype == null - || !InputMethodUtils.isValidSubtypeId(imi, mCurrentSubtype.hashCode())) { + || !SubtypeUtils.isValidSubtypeId(imi, mCurrentSubtype.hashCode())) { int subtypeId = mSettings.getSelectedInputMethodSubtypeId(selectedMethodId); if (subtypeId == NOT_A_SUBTYPE_ID) { // If there are no selected subtypes, the framework will try to find @@ -5241,17 +5242,16 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { mCurrentSubtype = explicitlyOrImplicitlyEnabledSubtypes.get(0); } else if (explicitlyOrImplicitlyEnabledSubtypes.size() > 1) { - mCurrentSubtype = InputMethodUtils.findLastResortApplicableSubtypeLocked( + mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( mRes, explicitlyOrImplicitlyEnabledSubtypes, - InputMethodUtils.SUBTYPE_MODE_KEYBOARD, null, true); + SubtypeUtils.SUBTYPE_MODE_KEYBOARD, null, true); if (mCurrentSubtype == null) { - mCurrentSubtype = InputMethodUtils.findLastResortApplicableSubtypeLocked( - mRes, explicitlyOrImplicitlyEnabledSubtypes, null, null, - true); + mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( + mRes, explicitlyOrImplicitlyEnabledSubtypes, null, null, true); } } } else { - mCurrentSubtype = InputMethodUtils.getSubtypes(imi).get(subtypeId); + mCurrentSubtype = SubtypeUtils.getSubtypes(imi).get(subtypeId); } } return mCurrentSubtype; @@ -5511,9 +5511,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // We cannot simply distinguish a bad IME that reports an arbitrary package name from // an unfortunate IME whose internal state is already obsolete due to the asynchronous // nature of our system. Let's compare it with our internal record. - if (!TextUtils.equals(mCurAttribute.packageName, packageName)) { - Slog.e(TAG, "Ignoring createInputContentUriToken mCurAttribute.packageName=" - + mCurAttribute.packageName + " packageName=" + packageName); + if (!TextUtils.equals(mCurEditorInfo.packageName, packageName)) { + Slog.e(TAG, "Ignoring createInputContentUriToken mCurEditorInfo.packageName=" + + mCurEditorInfo.packageName + " packageName=" + packageName); return null; } // This user ID can never bee spoofed. @@ -6168,8 +6168,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub setInputMethodEnabledLocked(inputMethodInfo.getId(), false); } // Re-enable with default enabled IMEs. - for (InputMethodInfo imi : - InputMethodUtils.getDefaultEnabledImes(mContext, mMethodList)) { + for (InputMethodInfo imi : InputMethodInfoUtils.getDefaultEnabledImes( + mContext, mMethodList)) { setInputMethodEnabledLocked(imi.getId(), true); } updateInputMethodsFromSettingsLocked(true /* enabledMayChange */); @@ -6190,8 +6190,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mContext.getResources(), mContext.getContentResolver(), methodMap, userId, false); - nextEnabledImes = InputMethodUtils.getDefaultEnabledImes(mContext, methodList); - nextIme = InputMethodUtils.getMostApplicableDefaultIME(nextEnabledImes).getId(); + nextEnabledImes = InputMethodInfoUtils.getDefaultEnabledImes(mContext, + methodList); + nextIme = InputMethodInfoUtils.getMostApplicableDefaultIME( + nextEnabledImes).getId(); // Reset enabled IMEs. settings.putEnabledInputMethodsStr(""); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java index c255fe14c03e..11e6923aa75a 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java @@ -105,7 +105,7 @@ final class InputMethodMenuController { if (currentSubtype != null) { final String curMethodId = mService.getSelectedMethodIdLocked(); final InputMethodInfo currentImi = mMethodMap.get(curMethodId); - lastInputMethodSubtypeId = InputMethodUtils.getSubtypeIdFromHashCode( + lastInputMethodSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode( currentImi, currentSubtype.hashCode()); } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java index f8894c64304d..a64322625797 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java @@ -241,8 +241,8 @@ final class InputMethodSubtypeSwitchingController { } private static int calculateSubtypeId(InputMethodInfo imi, InputMethodSubtype subtype) { - return subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi, - subtype.hashCode()) : NOT_A_SUBTYPE_ID; + return subtype != null ? SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()) + : NOT_A_SUBTYPE_ID; } private static class StaticRotationList { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java index 2d1a22e7552d..70132670e68e 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java @@ -27,7 +27,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Build; -import android.os.LocaleList; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; @@ -40,8 +39,6 @@ import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.textservice.SpellCheckerInfo; -import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.inputmethod.StartInputFlags; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; @@ -50,9 +47,7 @@ import com.android.server.textservices.TextServicesManagerInternal; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Locale; import java.util.function.Predicate; /** @@ -66,40 +61,13 @@ import java.util.function.Predicate; final class InputMethodUtils { public static final boolean DEBUG = false; static final int NOT_A_SUBTYPE_ID = -1; - private static final String SUBTYPE_MODE_ANY = null; - static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; private static final String TAG = "InputMethodUtils"; - private static final Locale ENGLISH_LOCALE = new Locale("en"); private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID); - private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE = - "EnabledWhenDefaultIsNotAsciiCapable"; // The string for enabled input method is saved as follows: // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0") private static final char INPUT_METHOD_SEPARATOR = ':'; private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';'; - /** - * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs - * that are mainly used until the system becomes ready. Note that {@link Locale} in this array - * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH} - * doesn't automatically match {@code Locale("en", "IN")}. - */ - private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = { - Locale.ENGLISH, // "en" - Locale.US, // "en_US" - Locale.UK, // "en_GB" - }; - - // A temporary workaround for the performance concerns in - // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo). - // TODO: Optimize all the critical paths including this one. - private static final Object sCacheLock = new Object(); - @GuardedBy("sCacheLock") - private static LocaleList sCachedSystemLocales; - @GuardedBy("sCacheLock") - private static InputMethodInfo sCachedInputMethodInfo; - @GuardedBy("sCacheLock") - private static ArrayList<InputMethodSubtype> sCachedResult; private InputMethodUtils() { // This utility class is not publicly instantiable. @@ -130,533 +98,6 @@ final class InputMethodUtils { } // ---------------------------------------------------------------------- - private static boolean isSystemImeThatHasSubtypeOf(InputMethodInfo imi, Context context, - boolean checkDefaultAttribute, @Nullable Locale requiredLocale, boolean checkCountry, - String requiredSubtypeMode) { - if (!imi.isSystem()) { - return false; - } - if (checkDefaultAttribute && !imi.isDefault(context)) { - return false; - } - if (!containsSubtypeOf(imi, requiredLocale, checkCountry, requiredSubtypeMode)) { - return false; - } - return true; - } - - @Nullable - private static Locale getFallbackLocaleForDefaultIme(ArrayList<InputMethodInfo> imis, - Context context) { - // At first, find the fallback locale from the IMEs that are declared as "default" in the - // current locale. Note that IME developers can declare an IME as "default" only for - // some particular locales but "not default" for other locales. - for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { - for (int i = 0; i < imis.size(); ++i) { - if (isSystemImeThatHasSubtypeOf(imis.get(i), context, - true /* checkDefaultAttribute */, fallbackLocale, - true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { - return fallbackLocale; - } - } - } - // If no fallback locale is found in the above condition, find fallback locales regardless - // of the "default" attribute as a last resort. - for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { - for (int i = 0; i < imis.size(); ++i) { - if (isSystemImeThatHasSubtypeOf(imis.get(i), context, - false /* checkDefaultAttribute */, fallbackLocale, - true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { - return fallbackLocale; - } - } - } - Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray())); - return null; - } - - private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(InputMethodInfo imi, - Context context, boolean checkDefaultAttribute) { - if (!imi.isSystem()) { - return false; - } - if (checkDefaultAttribute && !imi.isDefault(context)) { - return false; - } - if (!imi.isAuxiliaryIme()) { - return false; - } - final int subtypeCount = imi.getSubtypeCount(); - for (int i = 0; i < subtypeCount; ++i) { - final InputMethodSubtype s = imi.getSubtypeAt(i); - if (s.overridesImplicitlyEnabledSubtype()) { - return true; - } - } - return false; - } - - private static Locale getSystemLocaleFromContext(Context context) { - try { - return context.getResources().getConfiguration().locale; - } catch (Resources.NotFoundException ex) { - return null; - } - } - - private static final class InputMethodListBuilder { - // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration - // order can have non-trivial effect in the call sites. - @NonNull - private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>(); - - InputMethodListBuilder fillImes(ArrayList<InputMethodInfo> imis, Context context, - boolean checkDefaultAttribute, @Nullable Locale locale, boolean checkCountry, - String requiredSubtypeMode) { - for (int i = 0; i < imis.size(); ++i) { - final InputMethodInfo imi = imis.get(i); - if (isSystemImeThatHasSubtypeOf(imi, context, checkDefaultAttribute, locale, - checkCountry, requiredSubtypeMode)) { - mInputMethodSet.add(imi); - } - } - return this; - } - - // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be - // documented more clearly. - InputMethodListBuilder fillAuxiliaryImes(ArrayList<InputMethodInfo> imis, Context context) { - // If one or more auxiliary input methods are available, OK to stop populating the list. - for (final InputMethodInfo imi : mInputMethodSet) { - if (imi.isAuxiliaryIme()) { - return this; - } - } - boolean added = false; - for (int i = 0; i < imis.size(); ++i) { - final InputMethodInfo imi = imis.get(i); - if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, - true /* checkDefaultAttribute */)) { - mInputMethodSet.add(imi); - added = true; - } - } - if (added) { - return this; - } - for (int i = 0; i < imis.size(); ++i) { - final InputMethodInfo imi = imis.get(i); - if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, - false /* checkDefaultAttribute */)) { - mInputMethodSet.add(imi); - } - } - return this; - } - - public boolean isEmpty() { - return mInputMethodSet.isEmpty(); - } - - @NonNull - public ArrayList<InputMethodInfo> build() { - return new ArrayList<>(mInputMethodSet); - } - } - - private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale( - ArrayList<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale, - @Nullable Locale fallbackLocale) { - // Once the system becomes ready, we pick up at least one keyboard in the following order. - // Secondary users fall into this category in general. - // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true - // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false - // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true - // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false - // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true - // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false - // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. - - final InputMethodListBuilder builder = new InputMethodListBuilder(); - builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, - true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); - if (!builder.isEmpty()) { - return builder; - } - builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, - false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); - if (!builder.isEmpty()) { - return builder; - } - builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, - true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); - if (!builder.isEmpty()) { - return builder; - } - builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, - false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); - if (!builder.isEmpty()) { - return builder; - } - builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, - true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); - if (!builder.isEmpty()) { - return builder; - } - builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, - false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); - if (!builder.isEmpty()) { - return builder; - } - Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray()) - + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale); - return builder; - } - - static ArrayList<InputMethodInfo> getDefaultEnabledImes( - Context context, ArrayList<InputMethodInfo> imis, boolean onlyMinimum) { - final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context); - // We will primarily rely on the system locale, but also keep relying on the fallback locale - // as a last resort. - // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs), - // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic" - // subtype) - final Locale systemLocale = getSystemLocaleFromContext(context); - final InputMethodListBuilder builder = - getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale); - if (!onlyMinimum) { - builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, - true /* checkCountry */, SUBTYPE_MODE_ANY) - .fillAuxiliaryImes(imis, context); - } - return builder.build(); - } - - static ArrayList<InputMethodInfo> getDefaultEnabledImes( - Context context, ArrayList<InputMethodInfo> imis) { - return getDefaultEnabledImes(context, imis, false /* onlyMinimum */); - } - - /** - * Chooses an eligible system voice IME from the given IMEs. - * - * @param methodMap Map from the IME ID to {@link InputMethodInfo}. - * @param systemSpeechRecognizerPackageName System speech recognizer configured by the system - * config. - * @param currentDefaultVoiceImeId IME ID currently set to - * {@link Settings.Secure#DEFAULT_VOICE_INPUT_METHOD} - * @return {@link InputMethodInfo} that is found in {@code methodMap} and most suitable for - * the system voice IME. - */ - @Nullable - static InputMethodInfo chooseSystemVoiceIme( - @NonNull ArrayMap<String, InputMethodInfo> methodMap, - @Nullable String systemSpeechRecognizerPackageName, - @Nullable String currentDefaultVoiceImeId) { - if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) { - return null; - } - final InputMethodInfo defaultVoiceIme = methodMap.get(currentDefaultVoiceImeId); - // If the config matches the package of the setting, use the current one. - if (defaultVoiceIme != null && defaultVoiceIme.isSystem() - && defaultVoiceIme.getPackageName().equals(systemSpeechRecognizerPackageName)) { - return defaultVoiceIme; - } - InputMethodInfo firstMatchingIme = null; - final int methodCount = methodMap.size(); - for (int i = 0; i < methodCount; ++i) { - final InputMethodInfo imi = methodMap.valueAt(i); - if (!imi.isSystem()) { - continue; - } - if (!TextUtils.equals(imi.getPackageName(), systemSpeechRecognizerPackageName)) { - continue; - } - if (firstMatchingIme != null) { - Slog.e(TAG, "At most one InputMethodService can be published in " - + "systemSpeechRecognizer: " + systemSpeechRecognizerPackageName - + ". Ignoring all of them."); - return null; - } - firstMatchingIme = imi; - } - return firstMatchingIme; - } - - static boolean containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale, - boolean checkCountry, String mode) { - if (locale == null) { - return false; - } - final int N = imi.getSubtypeCount(); - for (int i = 0; i < N; ++i) { - final InputMethodSubtype subtype = imi.getSubtypeAt(i); - if (checkCountry) { - final Locale subtypeLocale = subtype.getLocaleObject(); - if (subtypeLocale == null || - !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) || - !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) { - continue; - } - } else { - final Locale subtypeLocale = new Locale(getLanguageFromLocaleString( - subtype.getLocale())); - if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) { - continue; - } - } - if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) || - mode.equalsIgnoreCase(subtype.getMode())) { - return true; - } - } - return false; - } - - static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) { - ArrayList<InputMethodSubtype> subtypes = new ArrayList<>(); - final int subtypeCount = imi.getSubtypeCount(); - for (int i = 0; i < subtypeCount; ++i) { - subtypes.add(imi.getSubtypeAt(i)); - } - return subtypes; - } - - static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) { - if (enabledImes == null || enabledImes.isEmpty()) { - return null; - } - // We'd prefer to fall back on a system IME, since that is safer. - int i = enabledImes.size(); - int firstFoundSystemIme = -1; - while (i > 0) { - i--; - final InputMethodInfo imi = enabledImes.get(i); - if (imi.isAuxiliaryIme()) { - continue; - } - if (imi.isSystem() && containsSubtypeOf( - imi, ENGLISH_LOCALE, false /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { - return imi; - } - if (firstFoundSystemIme < 0 && imi.isSystem()) { - firstFoundSystemIme = i; - } - } - return enabledImes.get(Math.max(firstFoundSystemIme, 0)); - } - - static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) { - return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID; - } - - static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) { - if (imi != null) { - final int subtypeCount = imi.getSubtypeCount(); - for (int i = 0; i < subtypeCount; ++i) { - InputMethodSubtype ims = imi.getSubtypeAt(i); - if (subtypeHashCode == ims.hashCode()) { - return i; - } - } - } - return NOT_A_SUBTYPE_ID; - } - - private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale = - new LocaleUtils.LocaleExtractor<InputMethodSubtype>() { - @Override - public Locale get(InputMethodSubtype source) { - return source != null ? source.getLocaleObject() : null; - } - }; - - @VisibleForTesting - static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked( - Resources res, InputMethodInfo imi) { - final LocaleList systemLocales = res.getConfiguration().getLocales(); - - synchronized (sCacheLock) { - // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because - // it does not check if subtypes are also identical. - if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) { - return new ArrayList<>(sCachedResult); - } - } - - // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl(). - // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive - // LocaleList rather than Resource. - final ArrayList<InputMethodSubtype> result = - getImplicitlyApplicableSubtypesLockedImpl(res, imi); - synchronized (sCacheLock) { - // Both LocaleList and InputMethodInfo are immutable. No need to copy them here. - sCachedSystemLocales = systemLocales; - sCachedInputMethodInfo = imi; - sCachedResult = new ArrayList<>(result); - } - return result; - } - - private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl( - Resources res, InputMethodInfo imi) { - final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi); - final LocaleList systemLocales = res.getConfiguration().getLocales(); - final String systemLocale = systemLocales.get(0).toString(); - if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>(); - final int numSubtypes = subtypes.size(); - - // Handle overridesImplicitlyEnabledSubtype mechanism. - final ArrayMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new ArrayMap<>(); - for (int i = 0; i < numSubtypes; ++i) { - // scan overriding implicitly enabled subtypes. - final InputMethodSubtype subtype = subtypes.get(i); - if (subtype.overridesImplicitlyEnabledSubtype()) { - final String mode = subtype.getMode(); - if (!applicableModeAndSubtypesMap.containsKey(mode)) { - applicableModeAndSubtypesMap.put(mode, subtype); - } - } - } - if (applicableModeAndSubtypesMap.size() > 0) { - return new ArrayList<>(applicableModeAndSubtypesMap.values()); - } - - final ArrayMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap = - new ArrayMap<>(); - final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>(); - - for (int i = 0; i < numSubtypes; ++i) { - final InputMethodSubtype subtype = subtypes.get(i); - final String mode = subtype.getMode(); - if (SUBTYPE_MODE_KEYBOARD.equals(mode)) { - keyboardSubtypes.add(subtype); - } else { - if (!nonKeyboardSubtypesMap.containsKey(mode)) { - nonKeyboardSubtypesMap.put(mode, new ArrayList<>()); - } - nonKeyboardSubtypesMap.get(mode).add(subtype); - } - } - - final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>(); - LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales, - applicableSubtypes); - - if (!applicableSubtypes.isEmpty()) { - boolean hasAsciiCapableKeyboard = false; - final int numApplicationSubtypes = applicableSubtypes.size(); - for (int i = 0; i < numApplicationSubtypes; ++i) { - final InputMethodSubtype subtype = applicableSubtypes.get(i); - if (subtype.isAsciiCapable()) { - hasAsciiCapableKeyboard = true; - break; - } - } - if (!hasAsciiCapableKeyboard) { - final int numKeyboardSubtypes = keyboardSubtypes.size(); - for (int i = 0; i < numKeyboardSubtypes; ++i) { - final InputMethodSubtype subtype = keyboardSubtypes.get(i); - final String mode = subtype.getMode(); - if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey( - TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) { - applicableSubtypes.add(subtype); - } - } - } - } - - if (applicableSubtypes.isEmpty()) { - InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked( - res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true); - if (lastResortKeyboardSubtype != null) { - applicableSubtypes.add(lastResortKeyboardSubtype); - } - } - - // For each non-keyboard mode, extract subtypes with system locales. - for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) { - LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales, - applicableSubtypes); - } - - return applicableSubtypes; - } - - /** - * Returns the language component of a given locale string. - * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)} - */ - private static String getLanguageFromLocaleString(String locale) { - final int idx = locale.indexOf('_'); - if (idx < 0) { - return locale; - } else { - return locale.substring(0, idx); - } - } - - /** - * If there are no selected subtypes, tries finding the most applicable one according to the - * given locale. - * @param subtypes this function will search the most applicable subtype in subtypes - * @param mode subtypes will be filtered by mode - * @param locale subtypes will be filtered by locale - * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype, - * it will return the first subtype matched with mode - * @return the most applicable subtypeId - */ - static InputMethodSubtype findLastResortApplicableSubtypeLocked( - Resources res, List<InputMethodSubtype> subtypes, String mode, String locale, - boolean canIgnoreLocaleAsLastResort) { - if (subtypes == null || subtypes.size() == 0) { - return null; - } - if (TextUtils.isEmpty(locale)) { - locale = res.getConfiguration().locale.toString(); - } - final String language = getLanguageFromLocaleString(locale); - boolean partialMatchFound = false; - InputMethodSubtype applicableSubtype = null; - InputMethodSubtype firstMatchedModeSubtype = null; - final int N = subtypes.size(); - for (int i = 0; i < N; ++i) { - InputMethodSubtype subtype = subtypes.get(i); - final String subtypeLocale = subtype.getLocale(); - final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale); - // An applicable subtype should match "mode". If mode is null, mode will be ignored, - // and all subtypes with all modes can be candidates. - if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) { - if (firstMatchedModeSubtype == null) { - firstMatchedModeSubtype = subtype; - } - if (locale.equals(subtypeLocale)) { - // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US") - applicableSubtype = subtype; - break; - } else if (!partialMatchFound && language.equals(subtypeLanguage)) { - // Partial match (e.g. system locale is "en_US" and subtype locale is "en") - applicableSubtype = subtype; - partialMatchFound = true; - } - } - } - - if (applicableSubtype == null && canIgnoreLocaleAsLastResort) { - return firstMatchedModeSubtype; - } - - // The first subtype applicable to the system locale will be defined as the most applicable - // subtype. - if (DEBUG) { - if (applicableSubtype != null) { - Slog.d(TAG, "Applicable InputMethodSubtype was found: " - + applicableSubtype.getMode() + "," + applicableSubtype.getLocale()); - } - } - return applicableSubtype; - } - static boolean canAddToLastInputMethod(InputMethodSubtype subtype) { if (subtype == null) return true; return !subtype.isAuxiliary(); @@ -790,6 +231,7 @@ final class InputMethodUtils { /** * Utility class for putting and getting settings for InputMethod * TODO: Move all putters and getters of settings to this class. + * TODO(b/235661780): Make the setting supports multi-users. */ public static class InputMethodSettings { private final TextUtils.SimpleStringSplitter mInputMethodSplitter = @@ -967,7 +409,7 @@ final class InputMethodUtils { List<InputMethodSubtype> enabledSubtypes = getEnabledInputMethodSubtypeListLocked(imi); if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) { - enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked( context.getResources(), imi); } return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes); @@ -1198,7 +640,7 @@ final class InputMethodUtils { // are enabled implicitly, so needs to treat them to be enabled. if (imi != null && imi.getSubtypeCount() > 0) { List<InputMethodSubtype> implicitlySelectedSubtypes = - getImplicitlyApplicableSubtypesLocked(mRes, imi); + SubtypeUtils.getImplicitlyApplicableSubtypesLocked(mRes, imi); if (implicitlySelectedSubtypes != null) { final int N = implicitlySelectedSubtypes.size(); for (int i = 0; i < N; ++i) { @@ -1216,7 +658,7 @@ final class InputMethodUtils { try { final int hashCode = Integer.parseInt(subtypeHashCode); // Check whether the subtype id is valid or not - if (isValidSubtypeId(imi, hashCode)) { + if (SubtypeUtils.isValidSubtypeId(imi, hashCode)) { return s; } else { return NOT_A_SUBTYPE_ID_STR; @@ -1336,7 +778,7 @@ final class InputMethodUtils { return NOT_A_SUBTYPE_ID; } final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode(); - return getSubtypeIdFromHashCode(imi, subtypeHashCode); + return SubtypeUtils.getSubtypeIdFromHashCode(imi, subtypeHashCode); } void saveCurrentInputMethodAndSubtypeToHistory(String curMethodId, diff --git a/services/core/java/com/android/server/inputmethod/LocaleUtils.java b/services/core/java/com/android/server/inputmethod/LocaleUtils.java index 7a6853a25e5b..3d02b3af6bc1 100644 --- a/services/core/java/com/android/server/inputmethod/LocaleUtils.java +++ b/services/core/java/com/android/server/inputmethod/LocaleUtils.java @@ -19,6 +19,8 @@ package com.android.server.inputmethod; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; import android.icu.util.ULocale; import android.os.LocaleList; import android.text.TextUtils; @@ -207,4 +209,25 @@ final class LocaleUtils { dest.add(sources.get(entry.mIndex)); } } + + /** + * Returns the language component of a given locale string. + * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)} + */ + static String getLanguageFromLocaleString(String locale) { + final int idx = locale.indexOf('_'); + if (idx < 0) { + return locale; + } else { + return locale.substring(0, idx); + } + } + + static Locale getSystemLocaleFromContext(Context context) { + try { + return context.getResources().getConfiguration().locale; + } catch (Resources.NotFoundException ex) { + return null; + } + } } diff --git a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java new file mode 100644 index 000000000000..eb85dd011288 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import android.annotation.Nullable; +import android.content.res.Resources; +import android.os.LocaleList; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * This class provides utility methods to handle and manage {@link InputMethodSubtype} for + * {@link InputMethodManagerService}. + * + * <p>This class is intentionally package-private. Utility methods here are tightly coupled with + * implementation details in {@link InputMethodManagerService}. Hence this class is not suitable + * for other components to directly use.</p> + */ +final class SubtypeUtils { + private static final String TAG = "SubtypeUtils"; + public static final boolean DEBUG = false; + + static final String SUBTYPE_MODE_ANY = null; + static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; + + static final int NOT_A_SUBTYPE_ID = -1; + private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE = + "EnabledWhenDefaultIsNotAsciiCapable"; + + // A temporary workaround for the performance concerns in + // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo). + // TODO: Optimize all the critical paths including this one. + // TODO(b/235661780): Make the cache supports multi-users. + private static final Object sCacheLock = new Object(); + @GuardedBy("sCacheLock") + private static LocaleList sCachedSystemLocales; + @GuardedBy("sCacheLock") + private static InputMethodInfo sCachedInputMethodInfo; + @GuardedBy("sCacheLock") + private static ArrayList<InputMethodSubtype> sCachedResult; + + static boolean containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale, + boolean checkCountry, String mode) { + if (locale == null) { + return false; + } + final int N = imi.getSubtypeCount(); + for (int i = 0; i < N; ++i) { + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + if (checkCountry) { + final Locale subtypeLocale = subtype.getLocaleObject(); + if (subtypeLocale == null || + !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) || + !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) { + continue; + } + } else { + final Locale subtypeLocale = new Locale(LocaleUtils.getLanguageFromLocaleString( + subtype.getLocale())); + if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) { + continue; + } + } + if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) || + mode.equalsIgnoreCase(subtype.getMode())) { + return true; + } + } + return false; + } + + static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) { + ArrayList<InputMethodSubtype> subtypes = new ArrayList<>(); + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + subtypes.add(imi.getSubtypeAt(i)); + } + return subtypes; + } + + static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) { + return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID; + } + + static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) { + if (imi != null) { + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + InputMethodSubtype ims = imi.getSubtypeAt(i); + if (subtypeHashCode == ims.hashCode()) { + return i; + } + } + } + return NOT_A_SUBTYPE_ID; + } + + private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale = + source -> source != null ? source.getLocaleObject() : null; + + @VisibleForTesting + static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked( + Resources res, InputMethodInfo imi) { + final LocaleList systemLocales = res.getConfiguration().getLocales(); + + synchronized (sCacheLock) { + // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because + // it does not check if subtypes are also identical. + if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) { + return new ArrayList<>(sCachedResult); + } + } + + // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl(). + // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive + // LocaleList rather than Resource. + final ArrayList<InputMethodSubtype> result = + getImplicitlyApplicableSubtypesLockedImpl(res, imi); + synchronized (sCacheLock) { + // Both LocaleList and InputMethodInfo are immutable. No need to copy them here. + sCachedSystemLocales = systemLocales; + sCachedInputMethodInfo = imi; + sCachedResult = new ArrayList<>(result); + } + return result; + } + + private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl( + Resources res, InputMethodInfo imi) { + final List<InputMethodSubtype> subtypes = getSubtypes(imi); + final LocaleList systemLocales = res.getConfiguration().getLocales(); + final String systemLocale = systemLocales.get(0).toString(); + if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>(); + final int numSubtypes = subtypes.size(); + + // Handle overridesImplicitlyEnabledSubtype mechanism. + final ArrayMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new ArrayMap<>(); + for (int i = 0; i < numSubtypes; ++i) { + // scan overriding implicitly enabled subtypes. + final InputMethodSubtype subtype = subtypes.get(i); + if (subtype.overridesImplicitlyEnabledSubtype()) { + final String mode = subtype.getMode(); + if (!applicableModeAndSubtypesMap.containsKey(mode)) { + applicableModeAndSubtypesMap.put(mode, subtype); + } + } + } + if (applicableModeAndSubtypesMap.size() > 0) { + return new ArrayList<>(applicableModeAndSubtypesMap.values()); + } + + final ArrayMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap = + new ArrayMap<>(); + final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>(); + + for (int i = 0; i < numSubtypes; ++i) { + final InputMethodSubtype subtype = subtypes.get(i); + final String mode = subtype.getMode(); + if (SUBTYPE_MODE_KEYBOARD.equals(mode)) { + keyboardSubtypes.add(subtype); + } else { + if (!nonKeyboardSubtypesMap.containsKey(mode)) { + nonKeyboardSubtypesMap.put(mode, new ArrayList<>()); + } + nonKeyboardSubtypesMap.get(mode).add(subtype); + } + } + + final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>(); + LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales, + applicableSubtypes); + + if (!applicableSubtypes.isEmpty()) { + boolean hasAsciiCapableKeyboard = false; + final int numApplicationSubtypes = applicableSubtypes.size(); + for (int i = 0; i < numApplicationSubtypes; ++i) { + final InputMethodSubtype subtype = applicableSubtypes.get(i); + if (subtype.isAsciiCapable()) { + hasAsciiCapableKeyboard = true; + break; + } + } + if (!hasAsciiCapableKeyboard) { + final int numKeyboardSubtypes = keyboardSubtypes.size(); + for (int i = 0; i < numKeyboardSubtypes; ++i) { + final InputMethodSubtype subtype = keyboardSubtypes.get(i); + final String mode = subtype.getMode(); + if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey( + TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) { + applicableSubtypes.add(subtype); + } + } + } + } + + if (applicableSubtypes.isEmpty()) { + InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked( + res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true); + if (lastResortKeyboardSubtype != null) { + applicableSubtypes.add(lastResortKeyboardSubtype); + } + } + + // For each non-keyboard mode, extract subtypes with system locales. + for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) { + LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales, + applicableSubtypes); + } + + return applicableSubtypes; + } + + /** + * If there are no selected subtypes, tries finding the most applicable one according to the + * given locale. + * @param subtypes this function will search the most applicable subtype in subtypes + * @param mode subtypes will be filtered by mode + * @param locale subtypes will be filtered by locale + * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype, + * it will return the first subtype matched with mode + * @return the most applicable subtypeId + */ + static InputMethodSubtype findLastResortApplicableSubtypeLocked( + Resources res, List<InputMethodSubtype> subtypes, String mode, String locale, + boolean canIgnoreLocaleAsLastResort) { + if (subtypes == null || subtypes.size() == 0) { + return null; + } + if (TextUtils.isEmpty(locale)) { + locale = res.getConfiguration().locale.toString(); + } + final String language = LocaleUtils.getLanguageFromLocaleString(locale); + boolean partialMatchFound = false; + InputMethodSubtype applicableSubtype = null; + InputMethodSubtype firstMatchedModeSubtype = null; + final int N = subtypes.size(); + for (int i = 0; i < N; ++i) { + InputMethodSubtype subtype = subtypes.get(i); + final String subtypeLocale = subtype.getLocale(); + final String subtypeLanguage = LocaleUtils.getLanguageFromLocaleString(subtypeLocale); + // An applicable subtype should match "mode". If mode is null, mode will be ignored, + // and all subtypes with all modes can be candidates. + if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) { + if (firstMatchedModeSubtype == null) { + firstMatchedModeSubtype = subtype; + } + if (locale.equals(subtypeLocale)) { + // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US") + applicableSubtype = subtype; + break; + } else if (!partialMatchFound && language.equals(subtypeLanguage)) { + // Partial match (e.g. system locale is "en_US" and subtype locale is "en") + applicableSubtype = subtype; + partialMatchFound = true; + } + } + } + + if (applicableSubtype == null && canIgnoreLocaleAsLastResort) { + return firstMatchedModeSubtype; + } + + // The first subtype applicable to the system locale will be defined as the most applicable + // subtype. + if (DEBUG) { + if (applicableSubtype != null) { + Slog.d(TAG, "Applicable InputMethodSubtype was found: " + + applicableSubtype.getMode() + "," + applicableSubtype.getLocale()); + } + } + return applicableSubtype; + } +} diff --git a/services/core/java/com/android/server/om/OverlayManagerShellCommand.java b/services/core/java/com/android/server/om/OverlayManagerShellCommand.java index bdde4f6ad86b..b05e44f9e625 100644 --- a/services/core/java/com/android/server/om/OverlayManagerShellCommand.java +++ b/services/core/java/com/android/server/om/OverlayManagerShellCommand.java @@ -62,7 +62,8 @@ final class OverlayManagerShellCommand extends ShellCommand { private final Context mContext; private final IOverlayManager mInterface; private static final Map<String, Integer> TYPE_MAP = Map.of( - "color", TypedValue.TYPE_FIRST_COLOR_INT); + "color", TypedValue.TYPE_FIRST_COLOR_INT, + "string", TypedValue.TYPE_STRING); OverlayManagerShellCommand(@NonNull final Context ctx, @NonNull final IOverlayManager iom) { mContext = ctx; @@ -390,13 +391,17 @@ final class OverlayManagerShellCommand extends ShellCommand { type = Integer.parseUnsignedInt(typeString); } } - final int intData; - if (valueString.startsWith("0x")) { - intData = Integer.parseUnsignedInt(valueString.substring(2), 16); + if (type == TypedValue.TYPE_STRING) { + overlayBuilder.setResourceValue(resourceName, type, valueString); } else { - intData = Integer.parseUnsignedInt(valueString); + final int intData; + if (valueString.startsWith("0x")) { + intData = Integer.parseUnsignedInt(valueString.substring(2), 16); + } else { + intData = Integer.parseUnsignedInt(valueString); + } + overlayBuilder.setResourceValue(resourceName, type, intData); } - overlayBuilder.setResourceValue(resourceName, type, intData); } private int runEnableExclusive() throws RemoteException { diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 109e7071469c..a909977583b4 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -6760,6 +6760,9 @@ public class PackageManagerService implements PackageSender, TestUtilityService } long deleteOatArtifactsOfPackage(@NonNull Computer snapshot, String packageName) { + PackageManagerServiceUtils.enforceSystemOrRootOrShell( + "Only the system or shell can delete oat artifacts"); + PackageStateInternal packageState = snapshot.getPackageStateInternal(packageName); if (packageState == null || packageState.getPkg() == null) { return -1; // error code of deleteOptimizedFiles diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index afcb21a45106..68f60152ef95 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -2942,9 +2942,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Set some sort of reasonable bounds on the size of the display that we will try // to emulate. final int minSize = 200; - final int maxScale = 2; - width = Math.min(Math.max(width, minSize), mInitialDisplayWidth * maxScale); - height = Math.min(Math.max(height, minSize), mInitialDisplayHeight * maxScale); + final int maxScale = 3; + final int maxSize = Math.max(mInitialDisplayWidth, mInitialDisplayHeight) * maxScale; + width = Math.min(Math.max(width, minSize), maxSize); + height = Math.min(Math.max(height, minSize), maxSize); } Slog.i(TAG_WM, "Using new display size: " + width + "x" + height); @@ -6194,6 +6195,14 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp .getKeyguardController().isAodShowing(mDisplayId); } + /** + * @return whether the keyguard is occluded on this display + */ + boolean isKeyguardOccluded() { + return mRootWindowContainer.mTaskSupervisor + .getKeyguardController().isDisplayOccluded(mDisplayId); + } + @VisibleForTesting void removeAllTasks() { forAllTasks((t) -> { t.getRootTask().removeChild(t, "removeAllTasks"); }); diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index d3af7d2a19e4..cff8b93ac947 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1212,7 +1212,8 @@ public class DisplayPolicy { break; default: if (attrs.providedInsets != null) { - for (InsetsFrameProvider provider : attrs.providedInsets) { + for (int i = attrs.providedInsets.length - 1; i >= 0; i--) { + final InsetsFrameProvider provider = attrs.providedInsets[i]; switch (provider.type) { case ITYPE_STATUS_BAR: mStatusBarAlt = win; @@ -1231,21 +1232,29 @@ public class DisplayPolicy { mExtraNavBarAltPosition = getAltBarPosition(attrs); break; } + // The index of the provider and corresponding insets types cannot change at + // runtime as ensured in WMS. Make use of the index in the provider directly + // to access the latest provided size at runtime. + final int index = i; final TriConsumer<DisplayFrames, WindowContainer, Rect> frameProvider = provider.insetsSize != null ? (displayFrames, windowContainer, inOutFrame) -> { inOutFrame.inset(win.mGivenContentInsets); + final InsetsFrameProvider ifp = + win.mAttrs.forRotation(displayFrames.mRotation) + .providedInsets[index]; calculateInsetsFrame(displayFrames, windowContainer, - inOutFrame, provider.source, - provider.insetsSize); + inOutFrame, ifp.source, ifp.insetsSize); } : null; final TriConsumer<DisplayFrames, WindowContainer, Rect> imeFrameProvider = provider.imeInsetsSize != null ? (displayFrames, windowContainer, inOutFrame) -> { inOutFrame.inset(win.mGivenContentInsets); + final InsetsFrameProvider ifp = + win.mAttrs.forRotation(displayFrames.mRotation) + .providedInsets[index]; calculateInsetsFrame(displayFrames, windowContainer, - inOutFrame, provider.source, - provider.imeInsetsSize); + inOutFrame, ifp.source, ifp.imeInsetsSize); } : null; mDisplayContent.setInsetProvider(provider.type, win, frameProvider, imeFrameProvider); @@ -1256,14 +1265,14 @@ public class DisplayPolicy { } } - private void calculateInsetsFrame(DisplayFrames df, WindowContainer coutainer, Rect inOutFrame, + private void calculateInsetsFrame(DisplayFrames df, WindowContainer container, Rect inOutFrame, int source, Insets insetsSize) { if (source == InsetsFrameProvider.SOURCE_DISPLAY) { inOutFrame.set(df.mUnrestricted); } else if (source == InsetsFrameProvider.SOURCE_CONTAINER_BOUNDS) { - inOutFrame.set(coutainer.getBounds()); + inOutFrame.set(container.getBounds()); } - if (insetsSize == null || insetsSize.equals(Insets.NONE)) { + if (insetsSize == null) { return; } // Only one side of the provider shall be applied. Check in the order of left - top - @@ -1276,6 +1285,8 @@ public class DisplayPolicy { inOutFrame.left = inOutFrame.right - insetsSize.right; } else if (insetsSize.bottom != 0) { inOutFrame.top = inOutFrame.bottom - insetsSize.bottom; + } else { + inOutFrame.setEmpty(); } } diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index d2c71f57e701..b9d83198139d 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -620,7 +620,8 @@ public class DisplayRotation { // We only enable seamless rotation if the top window has requested it and is in the // fullscreen opaque state. Seamless rotation requires freezing various Surface states and // won't work well with animations, so we disable it in the animation case for now. - if (w.getAttrs().rotationAnimation != ROTATION_ANIMATION_SEAMLESS || w.isAnimatingLw()) { + if (w.getAttrs().rotationAnimation != ROTATION_ANIMATION_SEAMLESS || w.inMultiWindowMode() + || w.isAnimatingLw()) { return false; } diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java index ad158c7b45b9..ac1a2b17603a 100644 --- a/services/core/java/com/android/server/wm/RemoteAnimationController.java +++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java @@ -331,8 +331,10 @@ class RemoteAnimationController implements DeathRecipient { private void invokeAnimationCancelled(String reason) { ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "cancelAnimation(): reason=%s", reason); + final boolean isKeyguardOccluded = mDisplayContent.isKeyguardOccluded(); + try { - mRemoteAnimationAdapter.getRunner().onAnimationCancelled(); + mRemoteAnimationAdapter.getRunner().onAnimationCancelled(isKeyguardOccluded); } catch (RemoteException e) { Slog.e(TAG, "Failed to notify cancel", e); } diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index ff6a45423084..9b013dac6adf 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -115,8 +115,6 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { private float mLastReportedAnimatorScale; private String mPackageName; private String mRelayoutTag; - private String mUpdateViewVisibilityTag; - private String mUpdateWindowLayoutTag; private final InsetsVisibilities mDummyRequestedVisibilities = new InsetsVisibilities(); private final InsetsSourceControl[] mDummyControls = new InsetsSourceControl[0]; final boolean mSetsUnrestrictedKeepClearAreas; @@ -225,27 +223,6 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } @Override - public int updateVisibility(IWindow client, WindowManager.LayoutParams attrs, - int viewVisibility, MergedConfiguration outMergedConfiguration, - SurfaceControl outSurfaceControl, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mUpdateViewVisibilityTag); - int res = mService.updateViewVisibility(this, client, attrs, viewVisibility, - outMergedConfiguration, outSurfaceControl, outInsetsState, outActiveControls); - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - return res; - } - - @Override - public void updateLayout(IWindow window, WindowManager.LayoutParams attrs, int flags, - ClientWindowFrames clientFrames, int requestedWidth, int requestedHeight) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mUpdateWindowLayoutTag); - mService.updateWindowLayout(this, window, attrs, flags, clientFrames, requestedWidth, - requestedHeight); - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - } - - @Override public void prepareToReplaceWindows(IBinder appToken, boolean childrenOnly) { mService.setWillReplaceWindows(appToken, childrenOnly); } @@ -717,8 +694,6 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { if (wpc != null) { mPackageName = wpc.mInfo.packageName; mRelayoutTag = "relayoutWindow: " + mPackageName; - mUpdateViewVisibilityTag = "updateVisibility: " + mPackageName; - mUpdateWindowLayoutTag = "updateLayout: " + mPackageName; } else { Slog.e(TAG_WM, "Unknown process pid=" + mPid); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index e764a05c343b..9d5b9455da13 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -122,6 +122,7 @@ import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN; import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION; +import static com.android.server.wm.WindowContainer.SYNC_STATE_NONE; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DISPLAY; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_INPUT_METHOD; @@ -2550,7 +2551,11 @@ public class WindowManagerService extends IWindowManager.Stub win.mLastSeqIdSentToRelayout = win.mSyncSeqId; outSyncIdBundle.putInt("seqid", win.mSyncSeqId); - win.mAlreadyRequestedSync = true; + // Only mark mAlreadyRequestedSync if there's an explicit sync request, and not if + // we're syncing due to mDrawHandlers + if (win.mSyncState != SYNC_STATE_NONE) { + win.mAlreadyRequestedSync = true; + } } else { outSyncIdBundle.putInt("seqid", -1); } @@ -2663,29 +2668,6 @@ public class WindowManagerService extends IWindowManager.Stub return result; } - int updateViewVisibility(Session session, IWindow client, LayoutParams attrs, - int viewVisibility, MergedConfiguration outMergedConfiguration, - SurfaceControl outSurfaceControl, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls) { - // TODO(b/161810301): Finish the implementation. - return 0; - } - - void updateWindowLayout(Session session, IWindow client, LayoutParams attrs, int flags, - ClientWindowFrames clientWindowFrames, int requestedWidth, int requestedHeight) { - final long origId = Binder.clearCallingIdentity(); - synchronized (mGlobalLock) { - final WindowState win = windowForClientLocked(session, client, false); - if (win == null) { - return; - } - win.setFrames(clientWindowFrames, requestedWidth, requestedHeight); - - // TODO(b/161810301): Finish the implementation. - } - Binder.restoreCallingIdentity(origId); - } - public boolean outOfMemoryWindow(Session session, IWindow client) { final long origId = Binder.clearCallingIdentity(); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java index 0c69067ab131..222a96d88166 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java @@ -315,8 +315,6 @@ class ActiveAdmin { public String mOrganizationId; public String mEnrollmentSpecificId; public boolean mAdminCanGrantSensorsPermissions; - public boolean mPreferentialNetworkServiceEnabled = - DevicePolicyManager.PREFERENTIAL_NETWORK_SERVICE_ENABLED_DEFAULT; public List<PreferentialNetworkServiceConfig> mPreferentialNetworkServiceConfigs = List.of(PreferentialNetworkServiceConfig.DEFAULT); @@ -848,15 +846,15 @@ class ActiveAdmin { } else if (TAG_ALWAYS_ON_VPN_LOCKDOWN.equals(tag)) { mAlwaysOnVpnLockdown = parser.getAttributeBoolean(null, ATTR_VALUE, false); } else if (TAG_PREFERENTIAL_NETWORK_SERVICE_ENABLED.equals(tag)) { - mPreferentialNetworkServiceEnabled = parser.getAttributeBoolean(null, ATTR_VALUE, + boolean preferentialNetworkServiceEnabled = parser.getAttributeBoolean(null, + ATTR_VALUE, DevicePolicyManager.PREFERENTIAL_NETWORK_SERVICE_ENABLED_DEFAULT); - if (mPreferentialNetworkServiceEnabled) { + if (preferentialNetworkServiceEnabled) { PreferentialNetworkServiceConfig.Builder configBuilder = new PreferentialNetworkServiceConfig.Builder(); - configBuilder.setEnabled(mPreferentialNetworkServiceEnabled); + configBuilder.setEnabled(preferentialNetworkServiceEnabled); configBuilder.setNetworkId(NET_ENTERPRISE_ID_1); mPreferentialNetworkServiceConfigs = List.of(configBuilder.build()); - mPreferentialNetworkServiceEnabled = false; } } else if (TAG_COMMON_CRITERIA_MODE.equals(tag)) { mCommonCriteriaMode = parser.getAttributeBoolean(null, ATTR_VALUE, false); @@ -1274,9 +1272,6 @@ class ActiveAdmin { pw.print("mAlwaysOnVpnLockdown="); pw.println(mAlwaysOnVpnLockdown); - pw.print("mPreferentialNetworkServiceEnabled="); - pw.println(mPreferentialNetworkServiceEnabled); - pw.print("mCommonCriteriaMode="); pw.println(mCommonCriteriaMode); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 388170bd24cb..a7ca08385b9f 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -7848,7 +7848,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { setDeviceOwner(); dpm.setDeviceOwnerType(admin1, DEVICE_OWNER_TYPE_FINANCED); when(getServices().ipackageManager.getBlockUninstallForUser( - eq(packageName), eq(UserHandle.USER_SYSTEM))) + eq(packageName), eq(UserHandle.getCallingUserId()))) .thenReturn(true); assertThat(dpm.isUninstallBlocked(admin1, packageName)).isTrue(); diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java index 0ed90d27db99..6a6cd6c914a2 100644 --- a/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java @@ -16,13 +16,11 @@ package com.android.server.display; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -34,17 +32,18 @@ import android.os.IThermalEventListener; import android.os.IThermalService; import android.os.Message; import android.os.PowerManager; -import android.os.Temperature.ThrottlingStatus; import android.os.Temperature; +import android.os.Temperature.ThrottlingStatus; import android.os.test.TestLooper; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.os.BackgroundThread; import com.android.server.display.BrightnessThrottler.Injector; -import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel; import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData; +import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel; import org.junit.Before; import org.junit.Test; @@ -55,7 +54,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; @SmallTest @@ -70,6 +68,8 @@ public class BrightnessThrottlerTest { @Mock IThermalService mThermalServiceMock; @Mock Injector mInjectorMock; + DisplayModeDirectorTest.FakeDeviceConfig mDeviceConfigFake; + @Captor ArgumentCaptor<IThermalEventListener> mThermalEventListenerCaptor; @Before @@ -83,6 +83,8 @@ public class BrightnessThrottlerTest { return true; } }); + mDeviceConfigFake = new DisplayModeDirectorTest.FakeDeviceConfig(); + when(mInjectorMock.getDeviceConfig()).thenReturn(mDeviceConfigFake); } @@ -292,6 +294,170 @@ public class BrightnessThrottlerTest { assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason()); } + @Test public void testUpdateThrottlingData() throws Exception { + // Initialise brightness throttling levels + // Ensure that they are overridden by setting the data through device config. + final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, + 0.25f); + List<ThrottlingLevel> levels = new ArrayList<>(); + levels.add(level); + final BrightnessThrottlingData data = BrightnessThrottlingData.create(levels); + mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,0.4"); + final BrightnessThrottler throttler = createThrottlerSupported(data); + + verify(mThermalServiceMock).registerThermalEventListenerWithType( + mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN)); + final IThermalEventListener listener = mThermalEventListenerCaptor.getValue(); + + // Set status too low to trigger throttling + listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f); + assertFalse(throttler.isThrottled()); + + // Set status high enough to trigger throttling + listener.notifyThrottling(getSkinTemp(level.thermalStatus)); + mTestLooper.dispatchAll(); + assertEquals(0.4f, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + + // Update thresholds + // This data is equivalent to the string "123,1,critical,0.8", passed below + final ThrottlingLevel newLevel = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, + 0.8f); + // Set new (valid) data from device config + mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,0.8"); + + // Set status too low to trigger throttling + listener.notifyThrottling(getSkinTemp(newLevel.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f); + assertFalse(throttler.isThrottled()); + + // Set status high enough to trigger throttling + listener.notifyThrottling(getSkinTemp(newLevel.thermalStatus)); + mTestLooper.dispatchAll(); + assertEquals(newLevel.brightness, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + } + + @Test public void testInvalidThrottlingStrings() throws Exception { + // Initialise brightness throttling levels + // Ensure that they are not overridden by invalid data through device config. + final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, + 0.25f); + List<ThrottlingLevel> levels = new ArrayList<>(); + levels.add(level); + final BrightnessThrottlingData data = BrightnessThrottlingData.create(levels); + final BrightnessThrottler throttler = createThrottlerSupported(data); + verify(mThermalServiceMock).registerThermalEventListenerWithType( + mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN)); + final IThermalEventListener listener = mThermalEventListenerCaptor.getValue(); + + // None of these are valid so shouldn't override the original data + mDeviceConfigFake.setBrightnessThrottlingData("321,1,critical,0.4"); // Not the current id + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData("123,0,critical,0.4"); // Incorrect number + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData("123,2,critical,0.4"); // Incorrect number + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData("123,1,invalid,0.4"); // Invalid level + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,none"); // Invalid brightness + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,-3"); // Invalid brightness + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData("invalid string"); // Invalid format + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + mDeviceConfigFake.setBrightnessThrottlingData(""); // Invalid format + testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f); + } + + private void testThrottling(BrightnessThrottler throttler, IThermalEventListener listener, + float tooLowCap, float tooHighCap) throws Exception { + final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, + tooHighCap); + + // Set status too low to trigger throttling + listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(tooLowCap, throttler.getBrightnessCap(), 0f); + assertFalse(throttler.isThrottled()); + + // Set status high enough to trigger throttling + listener.notifyThrottling(getSkinTemp(level.thermalStatus)); + mTestLooper.dispatchAll(); + assertEquals(tooHighCap, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + } + + @Test public void testMultipleConfigPoints() throws Exception { + // Initialise brightness throttling levels + final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, + 0.25f); + List<ThrottlingLevel> levels = new ArrayList<>(); + levels.add(level); + final BrightnessThrottlingData data = BrightnessThrottlingData.create(levels); + + // These are identical to the string set below + final ThrottlingLevel levelSevere = new ThrottlingLevel(PowerManager.THERMAL_STATUS_SEVERE, + 0.9f); + final ThrottlingLevel levelCritical = new ThrottlingLevel( + PowerManager.THERMAL_STATUS_CRITICAL, 0.5f); + final ThrottlingLevel levelEmergency = new ThrottlingLevel( + PowerManager.THERMAL_STATUS_EMERGENCY, 0.1f); + + mDeviceConfigFake.setBrightnessThrottlingData( + "123,3,severe,0.9,critical,0.5,emergency,0.1"); + final BrightnessThrottler throttler = createThrottlerSupported(data); + + verify(mThermalServiceMock).registerThermalEventListenerWithType( + mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN)); + final IThermalEventListener listener = mThermalEventListenerCaptor.getValue(); + + // Ensure that the multiple levels set via the string through the device config correctly + // override the original display device config ones. + + // levelSevere + // Set status too low to trigger throttling + listener.notifyThrottling(getSkinTemp(levelSevere.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f); + assertFalse(throttler.isThrottled()); + + // Set status high enough to trigger throttling + listener.notifyThrottling(getSkinTemp(levelSevere.thermalStatus)); + mTestLooper.dispatchAll(); + assertEquals(0.9f, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + + // levelCritical + // Set status too low to trigger throttling + listener.notifyThrottling(getSkinTemp(levelCritical.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(0.9f, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + + // Set status high enough to trigger throttling + listener.notifyThrottling(getSkinTemp(levelCritical.thermalStatus)); + mTestLooper.dispatchAll(); + assertEquals(0.5f, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + + //levelEmergency + // Set status too low to trigger throttling + listener.notifyThrottling(getSkinTemp(levelEmergency.thermalStatus - 1)); + mTestLooper.dispatchAll(); + assertEquals(0.5f, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + + // Set status high enough to trigger throttling + listener.notifyThrottling(getSkinTemp(levelEmergency.thermalStatus)); + mTestLooper.dispatchAll(); + assertEquals(0.1f, throttler.getBrightnessCap(), 0f); + assertTrue(throttler.isThrottled()); + } + private void assertThrottlingLevelsEquals( List<ThrottlingLevel> expected, List<ThrottlingLevel> actual) { @@ -307,12 +473,13 @@ public class BrightnessThrottlerTest { } private BrightnessThrottler createThrottlerUnsupported() { - return new BrightnessThrottler(mInjectorMock, mHandler, null, () -> {}); + return new BrightnessThrottler(mInjectorMock, mHandler, mHandler, null, () -> {}, null); } private BrightnessThrottler createThrottlerSupported(BrightnessThrottlingData data) { assertNotNull(data); - return new BrightnessThrottler(mInjectorMock, mHandler, data, () -> {}); + return new BrightnessThrottler(mInjectorMock, mHandler, BackgroundThread.getHandler(), + data, () -> {}, "123"); } private Temperature getSkinTemp(@ThrottlingStatus int status) { diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java index 864f31552ae3..968e1d8c546b 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java @@ -16,6 +16,7 @@ package com.android.server.display; +import static android.hardware.display.DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA; import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_HIGH_AMBIENT_BRIGHTNESS_THRESHOLDS; import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_HIGH_DISPLAY_BRIGHTNESS_THRESHOLDS; import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_LOW_AMBIENT_BRIGHTNESS_THRESHOLDS; @@ -1902,6 +1903,11 @@ public class DisplayModeDirectorTest { KEY_REFRESH_RATE_IN_HBM_HDR, String.valueOf(fps)); } + void setBrightnessThrottlingData(String brightnessThrottlingData) { + putPropertyAndNotify(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + KEY_BRIGHTNESS_THROTTLING_DATA, brightnessThrottlingData); + } + void setLowDisplayBrightnessThresholds(int[] brightnessThresholds) { String thresholds = toPropertyValue(brightnessThresholds); diff --git a/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java b/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java index 363c26b63bae..bbed1b60f8bf 100644 --- a/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java @@ -16,11 +16,14 @@ package com.android.server.display.color; +import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -1130,6 +1133,15 @@ public class ColorDisplayServiceTest { eq(ColorDisplayManager.COLOR_MODE_BOOSTED), any(), eq(Display.COLOR_MODE_INVALID)); } + @Test + public void getColorMode_noAvailableModes_returnsNotSet() { + when(mResourcesSpy.getIntArray(R.array.config_availableColorModes)) + .thenReturn(new int[] {}); + startService(); + verify(mDisplayTransformManager, never()).setColorMode(anyInt(), any(), anyInt()); + assertThat(mBinderService.getColorMode()).isEqualTo(-1); + } + /** * Configures Night display to use a custom schedule. * diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java index 6f1268e5de24..cc6f2cc5ba3e 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java +++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java @@ -275,7 +275,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_EN_US), imi); assertEquals(1, result.size()); verifyEquality(autoSubtype, result.get(0)); @@ -299,7 +299,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_EN_US), imi); assertEquals(2, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -323,7 +323,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_EN_GB), imi); assertEquals(2, result.size()); verifyEquality(nonAutoEnGB, result.get(0)); @@ -348,7 +348,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_FR), imi); assertEquals(2, result.size()); verifyEquality(nonAutoFrCA, result.get(0)); @@ -369,7 +369,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_FR_CA), imi); assertEquals(2, result.size()); verifyEquality(nonAutoFrCA, result.get(0)); @@ -391,7 +391,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_JA_JP), imi); assertEquals(3, result.size()); verifyEquality(nonAutoJa, result.get(0)); @@ -413,7 +413,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoHi, result.get(0)); @@ -430,7 +430,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -447,7 +447,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -469,7 +469,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(Locale.forLanguageTag("sr-Latn-RS")), imi); assertEquals(2, result.size()); assertThat(nonAutoSrLatn, is(in(result))); @@ -489,7 +489,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(Locale.forLanguageTag("sr-Cyrl-RS")), imi); assertEquals(2, result.size()); assertThat(nonAutoSrCyrl, is(in(result))); @@ -515,7 +515,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales( Locale.forLanguageTag("sr-Latn-RS-x-android"), Locale.forLanguageTag("ja-JP"), @@ -542,7 +542,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_FIL_PH), imi); assertEquals(1, result.size()); verifyEquality(nonAutoFil, result.get(0)); @@ -560,7 +560,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_FI), imi); assertEquals(1, result.size()); verifyEquality(nonAutoJa, result.get(0)); @@ -576,7 +576,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_IN), imi); assertEquals(1, result.size()); verifyEquality(nonAutoIn, result.get(0)); @@ -590,7 +590,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_ID), imi); assertEquals(1, result.size()); verifyEquality(nonAutoIn, result.get(0)); @@ -604,7 +604,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_IN), imi); assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); @@ -618,7 +618,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_ID), imi); assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); @@ -640,7 +640,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypesLocked( getResourcesForLocales(LOCALE_FR, LOCALE_EN_US, LOCALE_JA_JP), imi); assertThat(nonAutoFrCA, is(in(result))); assertThat(nonAutoEnUS, is(in(result))); @@ -680,26 +680,26 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_US, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_US, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_US, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_US, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_US, !CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_US, !CHECK_COUNTRY, SUBTYPE_MODE_VOICE)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_US, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_US, CHECK_COUNTRY, SUBTYPE_MODE_VOICE)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_US, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_US, !CHECK_COUNTRY, SUBTYPE_MODE_ANY)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_US, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_US, CHECK_COUNTRY, SUBTYPE_MODE_ANY)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_GB, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_GB, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_EN_GB, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_EN_GB, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); } @@ -711,22 +711,22 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI, !CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI_FI, !CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI_FI, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI_FI, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI_FI, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); } @@ -738,22 +738,22 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FIL_PH, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI, !CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI_FI, !CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI_FI, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertFalse(InputMethodUtils.containsSubtypeOf(imi, LOCALE_FI_FI, CHECK_COUNTRY, + assertFalse(SubtypeUtils.containsSubtypeOf(imi, LOCALE_FI_FI, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); } @@ -766,13 +766,13 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_IN, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_IN, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_IN, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_IN, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_ID, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_ID, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_ID, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_ID, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); } @@ -785,13 +785,13 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_IN, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_IN, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_IN, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_IN, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_ID, !CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_ID, !CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); - assertTrue(InputMethodUtils.containsSubtypeOf(imi, LOCALE_ID, CHECK_COUNTRY, + assertTrue(SubtypeUtils.containsSubtypeOf(imi, LOCALE_ID, CHECK_COUNTRY, SUBTYPE_MODE_KEYBOARD)); } } @@ -805,19 +805,19 @@ public class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, null, "")); + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, null, "")); } // Returns null when the config value is empty. { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, "", "")); + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, "", "")); } // Returns null when the configured package doesn't have an IME. { - assertNull(InputMethodUtils.chooseSystemVoiceIme(new ArrayMap<>(), + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(), systemIme.getPackageName(), "")); } @@ -825,7 +825,7 @@ public class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertEquals(systemIme, InputMethodUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, systemIme.getPackageName(), null)); } @@ -833,13 +833,13 @@ public class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertEquals(systemIme, InputMethodUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, systemIme.getPackageName(), "")); } // Returns null when the current default isn't found. { - assertNull(InputMethodUtils.chooseSystemVoiceIme(new ArrayMap<>(), + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(), systemIme.getPackageName(), systemIme.getId())); } @@ -850,8 +850,8 @@ public class InputMethodUtilsTest { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); methodMap.put(secondIme.getId(), secondIme); - assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, systemIme.getPackageName(), - "")); + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + systemIme.getPackageName(), "")); } // Returns the current one when the current default and config point to the same package. @@ -861,7 +861,7 @@ public class InputMethodUtilsTest { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); methodMap.put(secondIme.getId(), secondIme); - assertEquals(systemIme, InputMethodUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, systemIme.getPackageName(), systemIme.getId())); } @@ -871,7 +871,7 @@ public class InputMethodUtilsTest { final InputMethodInfo nonSystemIme = createFakeInputMethodInfo("NonSystemIme", "fake.voice0", false /* isSystem */); methodMap.put(nonSystemIme.getId(), nonSystemIme); - assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, nonSystemIme.getPackageName(), nonSystemIme.getId())); } @@ -882,7 +882,7 @@ public class InputMethodUtilsTest { "FakeDefaultAutoVoiceIme", "fake.voice0", false /* isSystem */); methodMap.put(systemIme.getId(), systemIme); methodMap.put(nonSystemIme.getId(), nonSystemIme); - assertNull(InputMethodUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, nonSystemIme.getPackageName(), "")); } } @@ -891,7 +891,7 @@ public class InputMethodUtilsTest { final Locale systemLocale, String... expectedImeNames) { final Context context = createTargetContextWithLocales(new LocaleList(systemLocale)); final String[] actualImeNames = getPackageNames( - InputMethodUtils.getDefaultEnabledImes(context, preinstalledImes)); + InputMethodInfoUtils.getDefaultEnabledImes(context, preinstalledImes)); assertEquals(expectedImeNames.length, actualImeNames.length); for (int i = 0; i < expectedImeNames.length; ++i) { assertEquals(expectedImeNames[i], actualImeNames[i]); @@ -902,7 +902,7 @@ public class InputMethodUtilsTest { final Locale systemLocale, String... expectedImeNames) { final Context context = createTargetContextWithLocales(new LocaleList(systemLocale)); final String[] actualImeNames = getPackageNames( - InputMethodUtils.getDefaultEnabledImes(context, preinstalledImes, + InputMethodInfoUtils.getDefaultEnabledImes(context, preinstalledImes, true /* onlyMinimum */)); assertEquals(expectedImeNames.length, actualImeNames.length); for (int i = 0; i < expectedImeNames.length; ++i) { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index cfeaf850da03..9de9582af6b5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -779,7 +779,7 @@ public class ActivityRecordTests extends WindowTestsBase { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { } }, 0, 0)); activity.updateOptionsLocked(opts); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java index 71f19148d616..b5764f54ff92 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java @@ -87,7 +87,7 @@ public class AppChangeTransitionTests extends WindowTestsBase { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { } @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java index 8656a4fecef1..f2d6273f2b26 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java @@ -806,7 +806,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { } @Override - public void onAnimationCancelled() throws RemoteException { + public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException { mFinishedCallback = null; } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java index 436cf36587d8..74154609b22e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java @@ -522,7 +522,7 @@ public class AppTransitionTests extends WindowTestsBase { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { mCancelled = true; } diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java index 204c7e6fb8d4..027f5218f820 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java @@ -43,6 +43,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -210,7 +211,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); adapter.onAnimationCancelled(mMockLeash); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); } @Test @@ -226,7 +227,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { mClock.fastForward(10500); mHandler.timeAdvance(); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), eq(adapter)); } @@ -247,12 +248,12 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { mClock.fastForward(10500); mHandler.timeAdvance(); - verify(mMockRunner, never()).onAnimationCancelled(); + verify(mMockRunner, never()).onAnimationCancelled(anyBoolean()); mClock.fastForward(52500); mHandler.timeAdvance(); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), eq(adapter)); } finally { @@ -264,7 +265,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { public void testZeroAnimations() throws Exception { mController.goodToGo(TRANSIT_OLD_NONE); verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any()); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); } @Test @@ -274,7 +275,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false); mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any()); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); } @Test @@ -316,7 +317,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { win.mActivityRecord.removeImmediately(); mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any()); - verify(mMockRunner).onAnimationCancelled(); + verify(mMockRunner).onAnimationCancelled(anyBoolean()); verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), eq(adapter)); } @@ -574,7 +575,7 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { // Cancel the wallpaper window animator and ensure the runner is not canceled wallpaperWindowToken.cancelAnimation(); - verify(mMockRunner, never()).onAnimationCancelled(); + verify(mMockRunner, never()).onAnimationCancelled(anyBoolean()); } finally { mDisplayContent.mOpeningApps.clear(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java index 5743922d0428..1715a295ded3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java @@ -979,7 +979,7 @@ public class WindowContainerTests extends WindowTestsBase { } @Override - public void onAnimationCancelled() { + public void onAnimationCancelled(boolean isKeyguardOccluded) { } }, 0, 0, false); adapter.setCallingPidUid(123, 456); diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index d1729f886f6e..432d08725e87 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -17069,4 +17069,35 @@ public class TelephonyManager { } return false; } + + /** + * Fetches the EFPSISMSC value from the SIM that contains the Public Service Identity + * of the SM-SC (either a SIP URI or tel URI), the value is common for both appType + * {@link #APPTYPE_ISIM} and {@link #APPTYPE_SIM}. + * The EFPSISMSC value is used by the ME to submit SMS over IP as defined in 24.341 [55]. + * + * @param appType ICC Application type {@link #APPTYPE_ISIM} or {@link #APPTYPE_USIM} + * @return SIP URI or tel URI of the Public Service Identity of the SM-SC + * @throws SecurityException if the caller does not have the required permission/privileges + * @hide + */ + @NonNull + @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) + public String getSmscIdentity(int appType) { + try { + IPhoneSubInfo info = getSubscriberInfoService(); + if (info == null) { + Rlog.e(TAG, "getSmscIdentity(): IPhoneSubInfo instance is NULL"); + return null; + } + /** Fetches the SIM PSISMSC params based on subId and appType */ + return info.getSmscIdentity(getSubId(), appType); + } catch (RemoteException ex) { + Rlog.e(TAG, "getSmscIdentity(): RemoteException: " + ex.getMessage()); + } catch (NullPointerException ex) { + Rlog.e(TAG, "getSmscIdentity(): NullPointerException: " + ex.getMessage()); + } + return null; + } } diff --git a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl index ce2017bb9a35..78335fd54917 100644 --- a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl +++ b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl @@ -218,4 +218,18 @@ interface IPhoneSubInfo { */ String getIccSimChallengeResponse(int subId, int appType, int authType, String data, String callingPackage, String callingFeatureId); + + /** + * Fetches the EFPSISMSC value from the SIM that contains the Public Service Identity + * of the SM-SC (either a SIP URI or tel URI), the value is common for both appType + * {@link #APPTYPE_ISIM} and {@link #APPTYPE_SIM}. + * The EFPSISMSC value is used by the ME to submit SMS over IP as defined in 24.341 [55]. + * + * @param appType ICC Application type {@link #APPTYPE_ISIM} or {@link #APPTYPE_USIM} + * @return SIP URI or tel URI of the Public Service Identity of the SM-SC + * @throws SecurityException if the caller does not have the required permission/privileges + * @hide + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)") + String getSmscIdentity(int subId, int appType); } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt index 23baac29b6c1..eee7cf3acbe6 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt @@ -16,14 +16,16 @@ package com.android.server.wm.flicker.launch +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.RequiresDevice -import android.platform.test.annotations.FlakyTest import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.navBarLayerPositionEnd +import com.android.server.wm.flicker.statusBarLayerPositionEnd import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -43,8 +45,8 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 @Postsubmit -open class OpenAppFromLockNotificationCold(testSpec: FlickerTestParameter) - : OpenAppFromNotificationCold(testSpec) { +open class OpenAppFromLockNotificationCold(testSpec: FlickerTestParameter) : + OpenAppFromNotificationCold(testSpec) { override val openingNotificationsFromLockScreen = true @@ -85,6 +87,62 @@ open class OpenAppFromLockNotificationCold(testSpec: FlickerTestParameter) @Test override fun appWindowBecomesTopWindow() = super.appWindowBecomesTopWindow() + /** + * Checks the position of the navigation bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerPositionEnd() + + /** + * Checks the position of the status bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerPositionEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appLayerBecomesVisible() = super.appLayerBecomesVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appWindowBecomesVisible() = super.appWindowBecomesVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + companion object { /** * Creates the test configurations. @@ -95,8 +153,8 @@ open class OpenAppFromLockNotificationCold(testSpec: FlickerTestParameter) @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { - return com.android.server.wm.flicker.FlickerTestParameterFactory.getInstance() + return FlickerTestParameterFactory.getInstance() .getConfigNonRotationTests(repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt index 49d8ea628585..b156eb1a0933 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt @@ -16,14 +16,16 @@ package com.android.server.wm.flicker.launch +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.RequiresDevice -import android.platform.test.annotations.FlakyTest import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.navBarLayerPositionEnd +import com.android.server.wm.flicker.statusBarLayerPositionEnd import com.android.server.wm.traces.common.FlickerComponentName import org.junit.FixMethodOrder import org.junit.Test @@ -44,8 +46,8 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 @Postsubmit -open class OpenAppFromLockNotificationWarm(testSpec: FlickerTestParameter) - : OpenAppFromNotificationWarm(testSpec) { +open class OpenAppFromLockNotificationWarm(testSpec: FlickerTestParameter) : + OpenAppFromNotificationWarm(testSpec) { override val openingNotificationsFromLockScreen = true @@ -111,6 +113,67 @@ open class OpenAppFromLockNotificationWarm(testSpec: FlickerTestParameter) override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() + /** + * Checks the position of the navigation bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerPositionEnd() + + /** + * Checks the position of the status bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerPositionEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appLayerBecomesVisible() = super.appLayerBecomesVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appWindowBecomesTopWindow() = super.appWindowBecomesTopWindow() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appWindowBecomesVisible() = super.appWindowBecomesVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + companion object { /** * Creates the test configurations. @@ -121,8 +184,8 @@ open class OpenAppFromLockNotificationWarm(testSpec: FlickerTestParameter) @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { - return com.android.server.wm.flicker.FlickerTestParameterFactory.getInstance() + return FlickerTestParameterFactory.getInstance() .getConfigNonRotationTests(repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt index 950f52ab57e1..5c86a4278b8c 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt @@ -16,9 +16,9 @@ package com.android.server.wm.flicker.launch +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.RequiresDevice -import android.platform.test.annotations.FlakyTest import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory @@ -26,6 +26,8 @@ import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.ShowWhenLockedAppHelper import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.navBarLayerPositionEnd +import com.android.server.wm.flicker.statusBarLayerPositionEnd import com.android.server.wm.traces.common.FlickerComponentName import org.junit.FixMethodOrder import org.junit.Test @@ -45,8 +47,8 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 @Postsubmit -class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParameter) - : OpenAppFromLockNotificationCold(testSpec) { +class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParameter) : + OpenAppFromLockNotificationCold(testSpec) { private val showWhenLockedApp: ShowWhenLockedAppHelper = ShowWhenLockedAppHelper(instrumentation) @@ -109,6 +111,73 @@ class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParamet @Test override fun entireScreenCovered() = super.entireScreenCovered() + /** + * Checks the position of the navigation bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerPositionEnd() + + /** + * Checks the position of the status bar at the start and end of the transition + * + * Differently from the normal usage of this assertion, check only the final state of the + * transition because the display is off at the start and the NavBar is never visible + */ + @Postsubmit + @Test + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerPositionEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appLayerBecomesVisible() = super.appLayerBecomesVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appWindowBecomesTopWindow() = super.appWindowBecomesTopWindow() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun appWindowBecomesVisible() = super.appWindowBecomesVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { /** * Creates the test configurations. @@ -123,4 +192,4 @@ class OpenAppFromLockNotificationWithLockOverlayApp(testSpec: FlickerTestParamet .getConfigNonRotationTests(repetitions = 3) } } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt index 2d00f3f92dcc..844311259a06 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt @@ -16,11 +16,11 @@ package com.android.server.wm.flicker.launch +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.RequiresDevice import android.view.WindowInsets import android.view.WindowManager -import android.platform.test.annotations.FlakyTest import androidx.test.uiautomator.By import androidx.test.uiautomator.Until import com.android.launcher3.tapl.LauncherInstrumentation @@ -53,8 +53,8 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group1 @Postsubmit -open class OpenAppFromNotificationWarm(testSpec: FlickerTestParameter) - : OpenAppTransition(testSpec) { +open class OpenAppFromNotificationWarm(testSpec: FlickerTestParameter) : + OpenAppTransition(testSpec) { protected val taplInstrumentation = LauncherInstrumentation() override val testApp: NotificationAppHelper = NotificationAppHelper(instrumentation) @@ -180,6 +180,32 @@ open class OpenAppFromNotificationWarm(testSpec: FlickerTestParameter) super.appWindowBecomesTopWindow() } + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { /** * Creates the test configurations. @@ -194,4 +220,4 @@ open class OpenAppFromNotificationWarm(testSpec: FlickerTestParameter) .getConfigNonRotationTests(repetitions = 3) } } -}
\ No newline at end of file +} |