Merge "[Autofill Framework] Update some confusing save related debug logs" into main
diff --git a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
index 470b1ec..1977a39 100644
--- a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
+++ b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl
@@ -24,6 +24,8 @@
import android.os.Bundle;
import android.app.ondeviceintelligence.Feature;
import android.app.ondeviceintelligence.FeatureDetails;
+ import android.app.ondeviceintelligence.InferenceInfo;
+ import java.util.List;
import android.app.ondeviceintelligence.IDownloadCallback;
import android.app.ondeviceintelligence.IListFeaturesCallback;
import android.app.ondeviceintelligence.IFeatureCallback;
@@ -72,4 +74,6 @@
in IStreamingResponseCallback streamingCallback) = 8;
String getRemoteServicePackageName() = 9;
+
+ List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) = 10;
}
diff --git a/core/java/android/app/ondeviceintelligence/InferenceInfo.aidl b/core/java/android/app/ondeviceintelligence/InferenceInfo.aidl
new file mode 100644
index 0000000..6d70fc4
--- /dev/null
+++ b/core/java/android/app/ondeviceintelligence/InferenceInfo.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.ondeviceintelligence;
+
+/**
+ * @hide
+ */
+parcelable InferenceInfo;
diff --git a/core/java/android/app/ondeviceintelligence/InferenceInfo.java b/core/java/android/app/ondeviceintelligence/InferenceInfo.java
new file mode 100644
index 0000000..5557a81
--- /dev/null
+++ b/core/java/android/app/ondeviceintelligence/InferenceInfo.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.ondeviceintelligence;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class represents the information related to an inference event to track the resource usage
+ * as a function of inference time.
+ *
+ * @hide
+ */
+public class InferenceInfo implements Parcelable {
+
+ /**
+ * Uid for the caller app.
+ */
+ private final int uid;
+
+ /**
+ * Inference start time (milliseconds from the epoch time).
+ */
+ private final long startTimeMs;
+
+ /**
+ * Inference end time (milliseconds from the epoch time).
+ */
+ private final long endTimeMs;
+
+ /**
+ * Suspended time in milliseconds.
+ */
+ private final long suspendedTimeMs;
+
+ /**
+ * Constructs an InferenceInfo object with the specified parameters.
+ *
+ * @param uid Uid for the caller app.
+ * @param startTimeMs Inference start time (milliseconds from the epoch time).
+ * @param endTimeMs Inference end time (milliseconds from the epoch time).
+ * @param suspendedTimeMs Suspended time in milliseconds.
+ */
+ public InferenceInfo(int uid, long startTimeMs, long endTimeMs,
+ long suspendedTimeMs) {
+ this.uid = uid;
+ this.startTimeMs = startTimeMs;
+ this.endTimeMs = endTimeMs;
+ this.suspendedTimeMs = suspendedTimeMs;
+ }
+
+ /**
+ * Constructs an InferenceInfo object from a Parcel.
+ *
+ * @param in The Parcel to read the object's data from.
+ */
+ protected InferenceInfo(Parcel in) {
+ uid = in.readInt();
+ startTimeMs = in.readLong();
+ endTimeMs = in.readLong();
+ suspendedTimeMs = in.readLong();
+ }
+
+
+ /**
+ * Writes the object's data to the provided Parcel.
+ *
+ * @param dest The Parcel to write the object's data to.
+ * @param flags Additional flags about how the object should be written.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(uid);
+ dest.writeLong(startTimeMs);
+ dest.writeLong(endTimeMs);
+ dest.writeLong(suspendedTimeMs);
+ }
+
+ /**
+ * Returns the UID for the caller app.
+ *
+ * @return the UID for the caller app.
+ */
+ public int getUid() {
+ return uid;
+ }
+
+ /**
+ * Returns the inference start time in milliseconds from the epoch time.
+ *
+ * @return the inference start time in milliseconds from the epoch time.
+ */
+ public long getStartTimeMs() {
+ return startTimeMs;
+ }
+
+ /**
+ * Returns the inference end time in milliseconds from the epoch time.
+ *
+ * @return the inference end time in milliseconds from the epoch time.
+ */
+ public long getEndTimeMs() {
+ return endTimeMs;
+ }
+
+ /**
+ * Returns the suspended time in milliseconds.
+ *
+ * @return the suspended time in milliseconds.
+ */
+ public long getSuspendedTimeMs() {
+ return suspendedTimeMs;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+
+ public static final @android.annotation.NonNull Parcelable.Creator<InferenceInfo> CREATOR
+ = new Parcelable.Creator<InferenceInfo>() {
+ @Override
+ public InferenceInfo[] newArray(int size) {
+ return new InferenceInfo[size];
+ }
+
+ @Override
+ public InferenceInfo createFromParcel(@android.annotation.NonNull Parcel in) {
+ return new InferenceInfo(in);
+ }
+ };
+
+ /**
+ * Builder class for creating instances of {@link InferenceInfo}.
+ */
+ public static class Builder {
+ private int uid;
+ private long startTimeMs;
+ private long endTimeMs;
+ private long suspendedTimeMs;
+
+ /**
+ * Sets the UID for the caller app.
+ *
+ * @param uid the UID for the caller app.
+ * @return the Builder instance.
+ */
+ public Builder setUid(int uid) {
+ this.uid = uid;
+ return this;
+ }
+
+ /**
+ * Sets the inference start time in milliseconds from the epoch time.
+ *
+ * @param startTimeMs the inference start time in milliseconds from the epoch time.
+ * @return the Builder instance.
+ */
+ public Builder setStartTimeMs(long startTimeMs) {
+ this.startTimeMs = startTimeMs;
+ return this;
+ }
+
+ /**
+ * Sets the inference end time in milliseconds from the epoch time.
+ *
+ * @param endTimeMs the inference end time in milliseconds from the epoch time.
+ * @return the Builder instance.
+ */
+ public Builder setEndTimeMs(long endTimeMs) {
+ this.endTimeMs = endTimeMs;
+ return this;
+ }
+
+ /**
+ * Sets the suspended time in milliseconds.
+ *
+ * @param suspendedTimeMs the suspended time in milliseconds.
+ * @return the Builder instance.
+ */
+ public Builder setSuspendedTimeMs(long suspendedTimeMs) {
+ this.suspendedTimeMs = suspendedTimeMs;
+ return this;
+ }
+
+ /**
+ * Builds and returns an instance of {@link InferenceInfo}.
+ *
+ * @return an instance of {@link InferenceInfo}.
+ */
+ public InferenceInfo build() {
+ return new InferenceInfo(uid, startTimeMs, endTimeMs,
+ suspendedTimeMs);
+ }
+ }
+}
diff --git a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
index a37f51b..937a9cd 100644
--- a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
+++ b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java
@@ -496,6 +496,24 @@
}
}
+ /**
+ * This is primarily intended to be used to attribute/blame on-device intelligence power usage,
+ * via the configured remote implementation, to its actual caller.
+ *
+ * @param startTimeEpochMillis epoch millis used to filter the InferenceInfo events.
+ * @return InferenceInfo events since the passed in startTimeEpochMillis.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) {
+ try {
+ return mService.getLatestInferenceInfo(startTimeEpochMillis);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/** Request inference with provided Bundle and Params. */
public static final int REQUEST_TYPE_INFERENCE = 0;
diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java
index 86d061c..e895d7b 100644
--- a/core/java/android/content/IntentFilter.java
+++ b/core/java/android/content/IntentFilter.java
@@ -823,6 +823,16 @@
}
/**
+ * Returns the number of actions in the filter, or {@code 0} if there are no actions.
+ * <p> This method provides a safe alternative to {@link #countActions()}, which
+ * may throw an exception if there are no actions.
+ * @hide
+ */
+ public final int safeCountActions() {
+ return mActions == null ? 0 : mActions.size();
+ }
+
+ /**
* Return an action in the filter.
*/
public final String getAction(int index) {
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
index a77e076..f123a96 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java
@@ -101,6 +101,11 @@
private static final String TAG = OnDeviceSandboxedInferenceService.class.getSimpleName();
/**
+ * @hide
+ */
+ public static final String INFERENCE_INFO_BUNDLE_KEY = "inference_info";
+
+ /**
* The {@link Intent} that must be declared as handled by the service. To be supported, the
* service must also require the
* {@link android.Manifest.permission#BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE}
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 2c63a7f..0d4c556 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -3010,16 +3010,34 @@
mLastAutofilledData.put(view.getAutofillId(), targetValue);
}
view.setAutofilled(true, hideHighlight);
+ if (sDebug) {
+ Log.d(TAG, "View " + view.getAutofillId() + " autofilled synchronously.");
+ }
try {
mService.setViewAutofilled(mSessionId, view.getAutofillId(), mContext.getUserId());
} catch (RemoteException e) {
// The failure could be a consequence of something going wrong on the server side.
// Do nothing here since it's just logging, but it's possible follow-up actions may
// fail.
+ Log.w(TAG, "Unable to log due to " + e);
+ }
+ } else {
+ if (sDebug) {
+ Log.d(TAG, "View " + view.getAutofillId() + " " + view.getClass().toString()
+ + " from " + view.getClass().getPackageName()
+ + " : didn't fill in synchronously. It may fill asynchronously.");
}
}
}
+ /**
+ * Returns String with text "null" if the object is null, or the actual string represented by
+ * the object.
+ */
+ private @NonNull String getString(Object obj) {
+ return obj == null ? "null" : obj.toString();
+ }
+
private void onGetCredentialException(int sessionId, AutofillId id, String errorType,
String errorMsg) {
synchronized (mLock) {
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index fd3837f..f7e0ec8 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -14051,6 +14051,9 @@
@Override
public void autofill(AutofillValue value) {
+ if (android.view.autofill.Helper.sVerbose) {
+ Log.v(LOG_TAG, "autofill() called on textview for id:" + getAutofillId());
+ }
if (!isTextAutofillable()) {
Log.w(LOG_TAG, "cannot autofill non-editable TextView: " + this);
return;
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 15f8c32..112eb61 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -111,3 +111,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "animate_bubble_size_change"
+ namespace: "multitasking"
+ description: "Turns on the animation for bubble bar icons size change"
+ bug: "335575529"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 0d23a6d..79be2b1 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -330,6 +330,8 @@
<string name="share_to_app_stop_dialog_title">Stop sharing screen?</string>
<!-- Text telling a user that they will stop sharing their screen if they click the "Stop sharing" button [CHAR LIMIT=100] -->
<string name="share_to_app_stop_dialog_message">You will stop sharing your screen</string>
+ <!-- Text telling a user that they will stop sharing the contents of the specified [app_name] if they click the "Stop sharing" button. Note that the app name will appear in bold. [CHAR LIMIT=100] -->
+ <string name="share_to_app_stop_dialog_message_specific_app">You will stop sharing <b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g></b></string>
<!-- Button to stop screen sharing [CHAR LIMIT=35] -->
<string name="share_to_app_stop_dialog_button">Stop sharing</string>
@@ -337,6 +339,8 @@
<string name="cast_to_other_device_stop_dialog_title">Stop casting screen?</string>
<!-- Text telling a user that they will stop casting their screen to a different device if they click the "Stop casting" button [CHAR LIMIT=100] -->
<string name="cast_to_other_device_stop_dialog_message">You will stop casting your screen</string>
+ <!-- Text telling a user that they will stop casting the contents of the specified [app_name] to a different device if they click the "Stop casting" button. Note that the app name will appear in bold. [CHAR LIMIT=100] -->
+ <string name="cast_to_other_device_stop_dialog_message_specific_app">You will stop casting <b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g></b></string>
<!-- Button to stop screen casting to a different device [CHAR LIMIT=35] -->
<string name="cast_to_other_device_stop_dialog_button">Stop casting</string>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
index 6611434..f6fbe38 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
@@ -30,8 +30,8 @@
import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor.Companion.createDialogLaunchOnClickListener
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndCastToOtherDeviceDialogDelegate
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndShareToAppDialogDelegate
-import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.Utils
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
@@ -60,8 +60,8 @@
private val mediaProjectionRepository: MediaProjectionRepository,
private val packageManager: PackageManager,
private val systemClock: SystemClock,
- private val dialogFactory: SystemUIDialog.Factory,
private val dialogTransitionAnimator: DialogTransitionAnimator,
+ private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
) : OngoingActivityChipInteractor {
override val chip: StateFlow<OngoingActivityChipModel> =
mediaProjectionRepository.mediaProjectionState
@@ -70,9 +70,9 @@
is MediaProjectionState.NotProjecting -> OngoingActivityChipModel.Hidden
is MediaProjectionState.Projecting -> {
if (isProjectionToOtherDevice(state.hostPackage)) {
- createCastToOtherDeviceChip()
+ createCastToOtherDeviceChip(state)
} else {
- createShareToAppChip()
+ createShareToAppChip(state)
}
}
}
@@ -97,7 +97,9 @@
return Utils.isHeadlessRemoteDisplayProvider(packageManager, packageName)
}
- private fun createCastToOtherDeviceChip(): OngoingActivityChipModel.Shown {
+ private fun createCastToOtherDeviceChip(
+ state: MediaProjectionState.Projecting,
+ ): OngoingActivityChipModel.Shown {
return OngoingActivityChipModel.Shown(
icon =
Icon.Resource(
@@ -107,32 +109,39 @@
// TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
startTimeMs = systemClock.elapsedRealtime(),
createDialogLaunchOnClickListener(
- castToOtherDeviceDialogDelegate,
+ createCastToOtherDeviceDialogDelegate(state),
dialogTransitionAnimator,
),
)
}
- private val castToOtherDeviceDialogDelegate =
+ private fun createCastToOtherDeviceDialogDelegate(state: MediaProjectionState.Projecting) =
EndCastToOtherDeviceDialogDelegate(
- dialogFactory,
+ endMediaProjectionDialogHelper,
this@MediaProjectionChipInteractor,
+ state,
)
- private fun createShareToAppChip(): OngoingActivityChipModel.Shown {
+ private fun createShareToAppChip(
+ state: MediaProjectionState.Projecting,
+ ): OngoingActivityChipModel.Shown {
return OngoingActivityChipModel.Shown(
// TODO(b/332662551): Use the right content description.
icon = Icon.Resource(SHARE_TO_APP_ICON, contentDescription = null),
// TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
startTimeMs = systemClock.elapsedRealtime(),
- createDialogLaunchOnClickListener(shareToAppDialogDelegate, dialogTransitionAnimator),
+ createDialogLaunchOnClickListener(
+ createShareToAppDialogDelegate(state),
+ dialogTransitionAnimator
+ ),
)
}
- private val shareToAppDialogDelegate =
+ private fun createShareToAppDialogDelegate(state: MediaProjectionState.Projecting) =
EndShareToAppDialogDelegate(
- dialogFactory,
+ endMediaProjectionDialogHelper,
this@MediaProjectionChipInteractor,
+ state,
)
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt
index 33cec97..596fbf8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt
@@ -17,25 +17,33 @@
package com.android.systemui.statusbar.chips.mediaprojection.ui.view
import android.os.Bundle
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
import com.android.systemui.statusbar.phone.SystemUIDialog
/** A dialog that lets the user stop an ongoing cast-screen-to-other-device event. */
class EndCastToOtherDeviceDialogDelegate(
- private val systemUIDialogFactory: SystemUIDialog.Factory,
+ private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
private val interactor: MediaProjectionChipInteractor,
+ private val state: MediaProjectionState.Projecting,
) : SystemUIDialog.Delegate {
override fun createDialog(): SystemUIDialog {
- return systemUIDialogFactory.create(this)
+ return endMediaProjectionDialogHelper.createDialog(this)
}
override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
with(dialog) {
setIcon(MediaProjectionChipInteractor.CAST_TO_OTHER_DEVICE_ICON)
setTitle(R.string.cast_to_other_device_stop_dialog_title)
- // TODO(b/332662551): Use a different message if they're sharing just a single app.
- setMessage(R.string.cast_to_other_device_stop_dialog_message)
+ setMessage(
+ endMediaProjectionDialogHelper.getDialogMessage(
+ state,
+ genericMessageResId = R.string.cast_to_other_device_stop_dialog_message,
+ specificAppMessageResId =
+ R.string.cast_to_other_device_stop_dialog_message_specific_app,
+ )
+ )
// No custom on-click, because the dialog will automatically be dismissed when the
// button is clicked anyway.
setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
new file mode 100644
index 0000000..347be02
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.mediaprojection.ui.view
+
+import android.annotation.StringRes
+import android.content.Context
+import android.content.pm.PackageManager
+import android.text.Html
+import android.text.Html.FROM_HTML_MODE_LEGACY
+import android.text.TextUtils
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+
+/** Helper class for showing dialogs that let users end different types of media projections. */
+@SysUISingleton
+class EndMediaProjectionDialogHelper
+@Inject
+constructor(
+ private val dialogFactory: SystemUIDialog.Factory,
+ private val packageManager: PackageManager,
+ private val context: Context
+) {
+ /** Creates a new [SystemUIDialog] using the given delegate. */
+ fun createDialog(delegate: SystemUIDialog.Delegate): SystemUIDialog {
+ return dialogFactory.create(delegate)
+ }
+
+ /**
+ * Returns the message to show in the dialog based on the specific media projection state.
+ *
+ * @param genericMessageResId a res ID for a more generic "end projection" message
+ * @param specificAppMessageResId a res ID for an "end projection" message that also lets us
+ * specify which app is currently being projected.
+ */
+ fun getDialogMessage(
+ state: MediaProjectionState.Projecting,
+ @StringRes genericMessageResId: Int,
+ @StringRes specificAppMessageResId: Int,
+ ): CharSequence {
+ when (state) {
+ is MediaProjectionState.Projecting.EntireScreen ->
+ return context.getString(genericMessageResId)
+ is MediaProjectionState.Projecting.SingleTask -> {
+ val packageName =
+ state.task.baseIntent.component?.packageName
+ ?: return context.getString(genericMessageResId)
+ try {
+ val appInfo = packageManager.getApplicationInfo(packageName, 0)
+ val appName = appInfo.loadLabel(packageManager)
+ return getSpecificAppMessageText(specificAppMessageResId, appName)
+ } catch (e: PackageManager.NameNotFoundException) {
+ // TODO(b/332662551): Log this error.
+ return context.getString(genericMessageResId)
+ }
+ }
+ }
+ }
+
+ private fun getSpecificAppMessageText(
+ @StringRes specificAppMessageResId: Int,
+ appName: CharSequence,
+ ): CharSequence {
+ // https://developer.android.com/guide/topics/resources/string-resource#StylingWithHTML
+ val escapedAppName = TextUtils.htmlEncode(appName.toString())
+ val text = context.getString(specificAppMessageResId, escapedAppName)
+ return Html.fromHtml(text, FROM_HTML_MODE_LEGACY)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt
index 3a863b1..749a11f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt
@@ -17,25 +17,32 @@
package com.android.systemui.statusbar.chips.mediaprojection.ui.view
import android.os.Bundle
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
import com.android.systemui.statusbar.phone.SystemUIDialog
/** A dialog that lets the user stop an ongoing share-screen-to-app event. */
class EndShareToAppDialogDelegate(
- private val systemUIDialogFactory: SystemUIDialog.Factory,
+ private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
private val interactor: MediaProjectionChipInteractor,
+ private val state: MediaProjectionState.Projecting,
) : SystemUIDialog.Delegate {
override fun createDialog(): SystemUIDialog {
- return systemUIDialogFactory.create(this)
+ return endMediaProjectionDialogHelper.createDialog(this)
}
override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
with(dialog) {
setIcon(MediaProjectionChipInteractor.SHARE_TO_APP_ICON)
setTitle(R.string.share_to_app_stop_dialog_title)
- // TODO(b/332662551): Use a different message if they're sharing just a single app.
- setMessage(R.string.share_to_app_stop_dialog_message)
+ setMessage(
+ endMediaProjectionDialogHelper.getDialogMessage(
+ state,
+ genericMessageResId = R.string.share_to_app_stop_dialog_message,
+ specificAppMessageResId = R.string.share_to_app_stop_dialog_message_specific_app
+ )
+ )
// No custom on-click, because the dialog will automatically be dismissed when the
// button is clicked anyway.
setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
index 11636bd..f95e0fb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
@@ -37,6 +37,7 @@
import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator
import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
+import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -224,8 +225,8 @@
modifiedStatusBarAttributes,
isTransientShown,
isInFullscreenMode,
- ongoingCallRepository.hasOngoingCall,
- ) { modifiedAttributes, isTransientShown, isInFullscreenMode, hasOngoingCall ->
+ ongoingCallRepository.ongoingCallState,
+ ) { modifiedAttributes, isTransientShown, isInFullscreenMode, ongoingCallState ->
if (modifiedAttributes == null) {
null
} else {
@@ -234,7 +235,7 @@
modifiedAttributes.appearance,
isTransientShown,
isInFullscreenMode,
- hasOngoingCall,
+ hasOngoingCall = ongoingCallState is OngoingCallModel.InCall,
)
StatusBarAppearance(
statusBarMode,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index a7d4ce3..d128057 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -29,35 +29,36 @@
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.CoreStartable
import com.android.systemui.Dumpable
-import com.android.systemui.res.R
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
+import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
import com.android.systemui.statusbar.policy.CallbackController
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
import java.io.PrintWriter
import java.util.concurrent.Executor
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
-/**
- * A controller to handle the ongoing call chip in the collapsed status bar.
- */
+/** A controller to handle the ongoing call chip in the collapsed status bar. */
@SysUISingleton
-class OngoingCallController @Inject constructor(
+class OngoingCallController
+@Inject
+constructor(
@Application private val scope: CoroutineScope,
private val context: Context,
private val ongoingCallRepository: OngoingCallRepository,
@@ -79,54 +80,61 @@
private val mListeners: MutableList<OngoingCallListener> = mutableListOf()
private val uidObserver = CallAppUidObserver()
- private val notifListener = object : NotifCollectionListener {
- // Temporary workaround for b/178406514 for testing purposes.
- //
- // b/178406514 means that posting an incoming call notif then updating it to an ongoing call
- // notif does not work (SysUI never receives the update). This workaround allows us to
- // trigger the ongoing call chip when an ongoing call notif is *added* rather than
- // *updated*, allowing us to test the chip.
- //
- // TODO(b/183229367): Remove this function override when b/178406514 is fixed.
- override fun onEntryAdded(entry: NotificationEntry) {
- onEntryUpdated(entry, true)
- }
+ private val notifListener =
+ object : NotifCollectionListener {
+ // Temporary workaround for b/178406514 for testing purposes.
+ //
+ // b/178406514 means that posting an incoming call notif then updating it to an ongoing
+ // call notif does not work (SysUI never receives the update). This workaround allows us
+ // to trigger the ongoing call chip when an ongoing call notif is *added* rather than
+ // *updated*, allowing us to test the chip.
+ //
+ // TODO(b/183229367): Remove this function override when b/178406514 is fixed.
+ override fun onEntryAdded(entry: NotificationEntry) {
+ onEntryUpdated(entry, true)
+ }
- override fun onEntryUpdated(entry: NotificationEntry) {
- // We have a new call notification or our existing call notification has been updated.
- // TODO(b/183229367): This likely won't work if you take a call from one app then
- // switch to a call from another app.
- if (callNotificationInfo == null && isCallNotification(entry) ||
- (entry.sbn.key == callNotificationInfo?.key)) {
- val newOngoingCallInfo = CallNotificationInfo(
- entry.sbn.key,
- entry.sbn.notification.getWhen(),
- entry.sbn.notification.contentIntent,
- entry.sbn.uid,
- entry.sbn.notification.extras.getInt(
- Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING,
- statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false
- )
- if (newOngoingCallInfo == callNotificationInfo) {
- return
+ override fun onEntryUpdated(entry: NotificationEntry) {
+ // We have a new call notification or our existing call notification has been
+ // updated.
+ // TODO(b/183229367): This likely won't work if you take a call from one app then
+ // switch to a call from another app.
+ if (
+ callNotificationInfo == null && isCallNotification(entry) ||
+ (entry.sbn.key == callNotificationInfo?.key)
+ ) {
+ val newOngoingCallInfo =
+ CallNotificationInfo(
+ entry.sbn.key,
+ entry.sbn.notification.getWhen(),
+ entry.sbn.notification.contentIntent,
+ entry.sbn.uid,
+ entry.sbn.notification.extras.getInt(
+ Notification.EXTRA_CALL_TYPE,
+ -1
+ ) == CALL_TYPE_ONGOING,
+ statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false
+ )
+ if (newOngoingCallInfo == callNotificationInfo) {
+ return
+ }
+
+ callNotificationInfo = newOngoingCallInfo
+ if (newOngoingCallInfo.isOngoing) {
+ updateChip()
+ } else {
+ removeChip()
+ }
}
+ }
- callNotificationInfo = newOngoingCallInfo
- if (newOngoingCallInfo.isOngoing) {
- updateChip()
- } else {
+ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+ if (entry.sbn.key == callNotificationInfo?.key) {
removeChip()
}
}
}
- override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
- if (entry.sbn.key == callNotificationInfo?.key) {
- removeChip()
- }
- }
- }
-
override fun start() {
dumpManager.registerDumpable(this)
notifCollection.addCollectionListener(notifListener)
@@ -169,8 +177,21 @@
*/
fun hasOngoingCall(): Boolean {
return callNotificationInfo?.isOngoing == true &&
- // When the user is in the phone app, don't show the chip.
- !uidObserver.isCallAppVisible
+ // When the user is in the phone app, don't show the chip.
+ !uidObserver.isCallAppVisible
+ }
+
+ /** Creates the right [OngoingCallModel] based on the call state. */
+ private fun getOngoingCallModel(): OngoingCallModel {
+ if (hasOngoingCall()) {
+ val currentInfo =
+ callNotificationInfo
+ // This shouldn't happen, but protect against it in case
+ ?: return OngoingCallModel.NoCall
+ return OngoingCallModel.InCall(currentInfo.callStartTime)
+ } else {
+ return OngoingCallModel.NoCall
+ }
}
override fun addCallback(listener: OngoingCallListener) {
@@ -182,9 +203,7 @@
}
override fun removeCallback(listener: OngoingCallListener) {
- synchronized(mListeners) {
- mListeners.remove(listener)
- }
+ synchronized(mListeners) { mListeners.remove(listener) }
}
private fun updateChip() {
@@ -196,8 +215,8 @@
if (currentChipView != null && timeView != null) {
if (currentCallNotificationInfo.hasValidStartTime()) {
timeView.setShouldHideText(false)
- timeView.base = currentCallNotificationInfo.callStartTime -
- systemClock.currentTimeMillis() +
+ timeView.base =
+ currentCallNotificationInfo.callStartTime - systemClock.currentTimeMillis() +
systemClock.elapsedRealtime()
timeView.start()
} else {
@@ -218,14 +237,19 @@
callNotificationInfo = null
if (DEBUG) {
- Log.w(TAG, "Ongoing call chip view could not be found; " +
- "Not displaying chip in status bar")
+ Log.w(
+ TAG,
+ "Ongoing call chip view could not be found; " +
+ "Not displaying chip in status bar"
+ )
}
}
}
private fun updateChipClickListener() {
- if (callNotificationInfo == null) { return }
+ if (callNotificationInfo == null) {
+ return
+ }
val currentChipView = chipView
val backgroundView =
currentChipView?.findViewById<View>(R.id.ongoing_activity_chip_background)
@@ -237,7 +261,8 @@
intent,
ActivityTransitionAnimator.Controller.fromView(
backgroundView,
- InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP)
+ InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
+ )
)
}
}
@@ -249,9 +274,11 @@
}
private fun updateGestureListening() {
- if (callNotificationInfo == null ||
- callNotificationInfo?.statusBarSwipedAway == true ||
- !isFullscreen) {
+ if (
+ callNotificationInfo == null ||
+ callNotificationInfo?.statusBarSwipedAway == true ||
+ !isFullscreen
+ ) {
swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG)
} else {
swipeStatusBarAwayGestureHandler.addOnGestureDetectedCallback(TAG) { _ ->
@@ -270,30 +297,31 @@
}
/** Tear down anything related to the chip view to prevent leaks. */
- @VisibleForTesting
- fun tearDownChipView() = chipView?.getTimeView()?.stop()
+ @VisibleForTesting fun tearDownChipView() = chipView?.getTimeView()?.stop()
private fun View.getTimeView(): ChipChronometer? {
return this.findViewById(R.id.ongoing_activity_chip_time)
}
/**
- * If there's an active ongoing call, then we will force the status bar to always show, even if
- * the user is in immersive mode. However, we also want to give users the ability to swipe away
- * the status bar if they need to access the area under the status bar.
- *
- * This method updates the status bar window appropriately when the swipe away gesture is
- * detected.
- */
+ * If there's an active ongoing call, then we will force the status bar to always show, even if
+ * the user is in immersive mode. However, we also want to give users the ability to swipe away
+ * the status bar if they need to access the area under the status bar.
+ *
+ * This method updates the status bar window appropriately when the swipe away gesture is
+ * detected.
+ */
private fun onSwipeAwayGestureDetected() {
- if (DEBUG) { Log.d(TAG, "Swipe away gesture detected") }
+ if (DEBUG) {
+ Log.d(TAG, "Swipe away gesture detected")
+ }
callNotificationInfo = callNotificationInfo?.copy(statusBarSwipedAway = true)
statusBarWindowController.setOngoingProcessRequiresStatusBarVisible(false)
swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG)
}
private fun sendStateChangeEvent() {
- ongoingCallRepository.setHasOngoingCall(hasOngoingCall())
+ ongoingCallRepository.setOngoingCallState(getOngoingCallModel())
mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
}
@@ -308,8 +336,8 @@
val statusBarSwipedAway: Boolean
) {
/**
- * Returns true if the notification information has a valid call start time.
- * See b/192379214.
+ * Returns true if the notification information has a valid call start time. See
+ * b/192379214.
*/
fun hasValidStartTime(): Boolean = callStartTime > 0
}
@@ -342,9 +370,10 @@
callAppUid = uid
try {
- isCallAppVisible = isProcessVisibleToUser(
- iActivityManager.getUidProcessState(uid, context.opPackageName)
- )
+ isCallAppVisible =
+ isProcessVisibleToUser(
+ iActivityManager.getUidProcessState(uid, context.opPackageName)
+ )
if (isRegistered) {
return
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/model/OngoingCallModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/model/OngoingCallModel.kt
new file mode 100644
index 0000000..aaa52a7b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/model/OngoingCallModel.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone.ongoingcall.data.model
+
+/** Represents the state of any ongoing calls. */
+sealed interface OngoingCallModel {
+ /** There is no ongoing call. */
+ data object NoCall : OngoingCallModel
+
+ /**
+ * There *is* an ongoing call.
+ *
+ * @property startTimeMs the time that the phone call started, based on the notification's
+ * `when` field. Importantly, this time is relative to
+ * [com.android.systemui.util.time.SystemClock.currentTimeMillis], **not**
+ * [com.android.systemui.util.time.SystemClock.elapsedRealtime].
+ */
+ data class InCall(val startTimeMs: Long) : OngoingCallModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepository.kt
index 886481e..554c474 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepository.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.phone.ongoingcall.data.repository
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -32,15 +33,15 @@
*/
@SysUISingleton
class OngoingCallRepository @Inject constructor() {
- private val _hasOngoingCall = MutableStateFlow(false)
- /** True if there's currently an ongoing call notification and false otherwise. */
- val hasOngoingCall: StateFlow<Boolean> = _hasOngoingCall.asStateFlow()
+ private val _ongoingCallState = MutableStateFlow<OngoingCallModel>(OngoingCallModel.NoCall)
+ /** The current ongoing call state. */
+ val ongoingCallState: StateFlow<OngoingCallModel> = _ongoingCallState.asStateFlow()
/**
- * Sets whether there's currently an ongoing call notification. Should only be set from
+ * Sets the current ongoing call state, based on notifications. Should only be set from
* [com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController].
*/
- fun setHasOngoingCall(hasOngoingCall: Boolean) {
- _hasOngoingCall.value = hasOngoingCall
+ fun setOngoingCallState(state: OngoingCallModel) {
+ _ongoingCallState.value = state
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfig.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfig.kt
index f4e3eab..0871c86 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfig.kt
@@ -18,6 +18,7 @@
import android.os.PersistableBundle
import android.telephony.CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL
+import android.telephony.CarrierConfigManager.KEY_SHOW_5G_SLICE_ICON_BOOL
import android.telephony.CarrierConfigManager.KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.flow.MutableStateFlow
@@ -66,10 +67,16 @@
/** Flow tracking the [KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL] config */
val showOperatorNameInStatusBar: StateFlow<Boolean> = showOperatorName.config
+ private val showNetworkSlice =
+ BooleanCarrierConfig(KEY_SHOW_5G_SLICE_ICON_BOOL, defaultConfig)
+ /** Flow tracking the [KEY_SHOW_5G_SLICE_ICON_BOOL] config */
+ val allowNetworkSliceIndicator: StateFlow<Boolean> = showNetworkSlice.config
+
private val trackedConfigs =
listOf(
inflateSignalStrength,
showOperatorName,
+ showNetworkSlice,
)
/** Ingest a new carrier config, and switch all of the tracked keys over to the new values */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 425c58b..205205e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -48,6 +48,9 @@
/** Reflects the value from the carrier config INFLATE_SIGNAL_STRENGTH for this connection */
val inflateSignalStrength: StateFlow<Boolean>
+ /** Carrier config KEY_SHOW_5G_SLICE_ICON_BOOL for this connection */
+ val allowNetworkSliceIndicator: StateFlow<Boolean>
+
/**
* The table log buffer created for this connection. Will have the name "MobileConnectionLog
* [subId]"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
index 83d5f2b..3261b71 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
@@ -79,6 +79,9 @@
)
.stateIn(scope, SharingStarted.WhileSubscribed(), _inflateSignalStrength.value)
+ // I don't see a reason why we would turn the config off for demo mode.
+ override val allowNetworkSliceIndicator = MutableStateFlow(true)
+
private val _isEmergencyOnly = MutableStateFlow(false)
override val isEmergencyOnly =
_isEmergencyOnly
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
index a532e62..2e47678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
@@ -166,6 +166,7 @@
override val isRoaming = MutableStateFlow(false).asStateFlow()
override val carrierId = MutableStateFlow(INVALID_SUBSCRIPTION_ID).asStateFlow()
override val inflateSignalStrength = MutableStateFlow(false).asStateFlow()
+ override val allowNetworkSliceIndicator = MutableStateFlow(false).asStateFlow()
override val isEmergencyOnly = MutableStateFlow(false).asStateFlow()
override val operatorAlphaShort = MutableStateFlow(null).asStateFlow()
override val isInService = MutableStateFlow(true).asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
index 41559b2..a5e47a6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -308,6 +308,21 @@
activeRepo.value.inflateSignalStrength.value
)
+ override val allowNetworkSliceIndicator =
+ activeRepo
+ .flatMapLatest { it.allowNetworkSliceIndicator }
+ .logDiffsForTable(
+ tableLogBuffer,
+ columnPrefix = "",
+ columnName = "allowSlice",
+ initialValue = activeRepo.value.allowNetworkSliceIndicator.value,
+ )
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ activeRepo.value.allowNetworkSliceIndicator.value
+ )
+
override val numberOfLevels =
activeRepo
.flatMapLatest { it.numberOfLevels }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index 6803a9d..9449659 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -310,6 +310,7 @@
.stateIn(scope, SharingStarted.WhileSubscribed(), UnknownNetworkType)
override val inflateSignalStrength = systemUiCarrierConfig.shouldInflateSignalStrength
+ override val allowNetworkSliceIndicator = systemUiCarrierConfig.allowNetworkSliceIndicator
override val numberOfLevels =
inflateSignalStrength
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index ed9e405..507759c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -253,7 +253,13 @@
)
override val showSliceAttribution: StateFlow<Boolean> =
- connectionRepository.hasPrioritizedNetworkCapabilities
+ combine(
+ connectionRepository.allowNetworkSliceIndicator,
+ connectionRepository.hasPrioritizedNetworkCapabilities,
+ ) { allowed, hasPrioritizedNetworkCapabilities ->
+ allowed && hasPrioritizedNetworkCapabilities
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val isNonTerrestrial: StateFlow<Boolean> =
if (Flags.carrierEnabledSatelliteFlag()) {
@@ -350,7 +356,8 @@
shownLevel.map {
SignalIconModel.Satellite(
level = it,
- icon = SatelliteIconModel.fromSignalStrength(it)
+ icon =
+ SatelliteIconModel.fromSignalStrength(it)
?: SatelliteIconModel.fromSignalStrength(0)!!
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
index a4505a9..327eec4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
@@ -28,6 +28,7 @@
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
@@ -55,7 +56,7 @@
@SmallTest
class MediaProjectionChipInteractorTest : SysuiTestCase() {
- private val kosmos = Kosmos()
+ private val kosmos = Kosmos().also { it.testCase = this }
private val testScope = kosmos.testScope
private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository
private val systemClock = kosmos.fakeSystemClock
@@ -178,7 +179,7 @@
}
@Test
- fun chip_castToOtherDevice_clickListenerShowsCastDialog() =
+ fun chip_castToOtherDevice_entireScreen_clickListenerShowsCastDialog() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
mediaProjectionRepo.mediaProjectionState.value =
@@ -200,7 +201,33 @@
}
@Test
- fun chip_shareToApp_clickListenerShowsShareDialog() =
+ fun chip_castToOtherDevice_singleTask_clickListenerShowsCastDialog() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(
+ CAST_TO_OTHER_DEVICES_PACKAGE,
+ createTask(taskId = 1)
+ )
+
+ val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+
+ // Dialogs must be created on the main thread
+ context.mainExecutor.execute {
+ clickListener.onClick(chipView)
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockCastDialog),
+ eq(chipBackgroundView),
+ eq(null),
+ anyBoolean(),
+ )
+ }
+ }
+
+ @Test
+ fun chip_shareToApp_entireScreen_clickListenerShowsShareDialog() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
mediaProjectionRepo.mediaProjectionState.value =
@@ -221,6 +248,28 @@
}
}
+ @Test
+ fun chip_shareToApp_singleTask_clickListenerShowsShareDialog() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.SingleTask(NORMAL_PACKAGE, createTask(taskId = 1))
+
+ val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+
+ // Dialogs must be created on the main thread
+ context.mainExecutor.execute {
+ clickListener.onClick(chipView)
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockShareDialog),
+ eq(chipBackgroundView),
+ eq(null),
+ anyBoolean(),
+ )
+ }
+ }
+
companion object {
const val CAST_TO_OTHER_DEVICES_PACKAGE = "other.devices.package"
const val NORMAL_PACKAGE = "some.normal.package"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt
index 9a2f545..7b676e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt
@@ -16,22 +16,28 @@
package com.android.systemui.statusbar.chips.mediaprojection.ui.view
+import android.content.ComponentName
import android.content.DialogInterface
+import android.content.Intent
+import android.content.packageManager
+import android.content.pm.ApplicationInfo
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
import com.android.systemui.statusbar.phone.SystemUIDialog
-import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.Before
+import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
@@ -41,22 +47,14 @@
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
class EndCastToOtherDeviceDialogDelegateTest : SysuiTestCase() {
- private val kosmos = Kosmos()
+ private val kosmos = Kosmos().also { it.testCase = this }
private val sysuiDialog = mock<SystemUIDialog>()
- private val sysuiDialogFactory = kosmos.mockSystemUIDialogFactory
- private val underTest =
- EndCastToOtherDeviceDialogDelegate(
- sysuiDialogFactory,
- kosmos.mediaProjectionChipInteractor,
- )
-
- @Before
- fun setUp() {
- whenever(sysuiDialogFactory.create(eq(underTest), eq(context))).thenReturn(sysuiDialog)
- }
+ private lateinit var underTest: EndCastToOtherDeviceDialogDelegate
@Test
fun icon() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
verify(sysuiDialog).setIcon(R.drawable.ic_cast_connected)
@@ -64,20 +62,52 @@
@Test
fun title() {
+ createAndSetDelegate(SINGLE_TASK)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
verify(sysuiDialog).setTitle(R.string.cast_to_other_device_stop_dialog_title)
}
@Test
- fun message() {
+ fun message_entireScreen() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
- verify(sysuiDialog).setMessage(R.string.cast_to_other_device_stop_dialog_message)
+ verify(sysuiDialog)
+ .setMessage(context.getString(R.string.cast_to_other_device_stop_dialog_message))
+ }
+
+ @Test
+ fun message_singleTask() {
+ val baseIntent =
+ Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+ val appInfo = mock<ApplicationInfo>()
+ whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package")
+ whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+ .thenReturn(appInfo)
+
+ createAndSetDelegate(
+ MediaProjectionState.Projecting.SingleTask(
+ HOST_PACKAGE,
+ createTask(taskId = 1, baseIntent = baseIntent)
+ )
+ )
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ // It'd be nice to use R.string.cast_to_other_device_stop_dialog_message_specific_app
+ // directly, but it includes the <b> tags which aren't in the returned string.
+ val result = argumentCaptor<CharSequence>()
+ verify(sysuiDialog).setMessage(result.capture())
+ assertThat(result.firstValue.toString()).isEqualTo("You will stop casting Fake Package")
}
@Test
fun negativeButton() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null)
@@ -86,6 +116,8 @@
@Test
fun positiveButton() =
kosmos.testScope.runTest {
+ createAndSetDelegate(SINGLE_TASK)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
@@ -105,4 +137,20 @@
assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
}
+
+ private fun createAndSetDelegate(state: MediaProjectionState.Projecting) {
+ underTest =
+ EndCastToOtherDeviceDialogDelegate(
+ kosmos.endMediaProjectionDialogHelper,
+ kosmos.mediaProjectionChipInteractor,
+ state,
+ )
+ }
+
+ companion object {
+ private const val HOST_PACKAGE = "fake.host.package"
+ private val ENTIRE_SCREEN = MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE)
+ private val SINGLE_TASK =
+ MediaProjectionState.Projecting.SingleTask(HOST_PACKAGE, createTask(taskId = 1))
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
new file mode 100644
index 0000000..bbd1109
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.mediaprojection.ui.view
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.packageManager
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+class EndMediaProjectionDialogHelperTest : SysuiTestCase() {
+ private val kosmos = Kosmos().also { it.testCase = this }
+
+ private val underTest = kosmos.endMediaProjectionDialogHelper
+
+ @Test
+ fun createDialog_usesDelegateAndFactory() {
+ val dialog = mock<SystemUIDialog>()
+ val delegate = SystemUIDialog.Delegate { dialog }
+ whenever(kosmos.mockSystemUIDialogFactory.create(eq(delegate))).thenReturn(dialog)
+
+ underTest.createDialog(delegate)
+
+ verify(kosmos.mockSystemUIDialogFactory).create(delegate)
+ }
+
+ @Test
+ fun getDialogMessage_entireScreen_isGenericMessage() {
+ val result =
+ underTest.getDialogMessage(
+ MediaProjectionState.Projecting.EntireScreen("host.package"),
+ R.string.accessibility_home,
+ R.string.cast_to_other_device_stop_dialog_message_specific_app
+ )
+
+ assertThat(result).isEqualTo(context.getString(R.string.accessibility_home))
+ }
+
+ @Test
+ fun getDialogMessage_singleTask_cannotFindPackage_isGenericMessage() {
+ val baseIntent =
+ Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+ whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+ .thenThrow(PackageManager.NameNotFoundException())
+
+ val projectionState =
+ MediaProjectionState.Projecting.SingleTask(
+ "host.package",
+ createTask(taskId = 1, baseIntent = baseIntent)
+ )
+
+ val result =
+ underTest.getDialogMessage(
+ projectionState,
+ R.string.accessibility_home,
+ R.string.cast_to_other_device_stop_dialog_message_specific_app
+ )
+
+ assertThat(result).isEqualTo(context.getString(R.string.accessibility_home))
+ }
+
+ @Test
+ fun getDialogMessage_singleTask_findsPackage_isSpecificMessageWithAppLabel() {
+ val baseIntent =
+ Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+ val appInfo = mock<ApplicationInfo>()
+ whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package")
+ whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+ .thenReturn(appInfo)
+
+ val projectionState =
+ MediaProjectionState.Projecting.SingleTask(
+ "host.package",
+ createTask(taskId = 1, baseIntent = baseIntent)
+ )
+
+ val result =
+ underTest.getDialogMessage(
+ projectionState,
+ R.string.accessibility_home,
+ R.string.cast_to_other_device_stop_dialog_message_specific_app
+ )
+
+ // It'd be nice to use the R.string resources directly, but they include the <b> tags which
+ // aren't in the returned string.
+ assertThat(result.toString()).isEqualTo("You will stop casting Fake Package")
+ }
+
+ @Test
+ fun getDialogMessage_appLabelHasSpecialCharacters_isEscaped() {
+ val baseIntent =
+ Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+ val appInfo = mock<ApplicationInfo>()
+ whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake & Package <Here>")
+ whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+ .thenReturn(appInfo)
+
+ val projectionState =
+ MediaProjectionState.Projecting.SingleTask(
+ "host.package",
+ createTask(taskId = 1, baseIntent = baseIntent)
+ )
+
+ val result =
+ underTest.getDialogMessage(
+ projectionState,
+ R.string.accessibility_home,
+ R.string.cast_to_other_device_stop_dialog_message_specific_app
+ )
+
+ assertThat(result.toString()).isEqualTo("You will stop casting Fake & Package <Here>")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt
index 1d6e866..4ddca52 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt
@@ -16,22 +16,28 @@
package com.android.systemui.statusbar.chips.mediaprojection.ui.view
+import android.content.ComponentName
import android.content.DialogInterface
+import android.content.Intent
+import android.content.packageManager
+import android.content.pm.ApplicationInfo
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
import com.android.systemui.statusbar.phone.SystemUIDialog
-import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.Before
+import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
@@ -41,22 +47,14 @@
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
class EndShareToAppDialogDelegateTest : SysuiTestCase() {
- private val kosmos = Kosmos()
+ private val kosmos = Kosmos().also { it.testCase = this }
private val sysuiDialog = mock<SystemUIDialog>()
- private val sysuiDialogFactory = kosmos.mockSystemUIDialogFactory
- private val underTest =
- EndShareToAppDialogDelegate(
- sysuiDialogFactory,
- kosmos.mediaProjectionChipInteractor,
- )
-
- @Before
- fun setUp() {
- whenever(sysuiDialogFactory.create(eq(underTest), eq(context))).thenReturn(sysuiDialog)
- }
+ private lateinit var underTest: EndShareToAppDialogDelegate
@Test
fun icon() {
+ createAndSetDelegate(SINGLE_TASK)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
verify(sysuiDialog).setIcon(R.drawable.ic_screenshot_share)
@@ -64,20 +62,51 @@
@Test
fun title() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
verify(sysuiDialog).setTitle(R.string.share_to_app_stop_dialog_title)
}
@Test
- fun message() {
+ fun message_entireScreen() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
- verify(sysuiDialog).setMessage(R.string.share_to_app_stop_dialog_message)
+ verify(sysuiDialog).setMessage(context.getString(R.string.share_to_app_stop_dialog_message))
+ }
+
+ @Test
+ fun message_singleTask() {
+ val baseIntent =
+ Intent().apply { this.component = ComponentName("fake.task.package", "cls") }
+ val appInfo = mock<ApplicationInfo>()
+ whenever(appInfo.loadLabel(kosmos.packageManager)).thenReturn("Fake Package")
+ whenever(kosmos.packageManager.getApplicationInfo(eq("fake.task.package"), any<Int>()))
+ .thenReturn(appInfo)
+
+ createAndSetDelegate(
+ MediaProjectionState.Projecting.SingleTask(
+ HOST_PACKAGE,
+ createTask(taskId = 1, baseIntent = baseIntent)
+ )
+ )
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ // It'd be nice to use R.string.share_to_app_stop_dialog_message_specific_app directly, but
+ // it includes the <b> tags which aren't in the returned string.
+ val result = argumentCaptor<CharSequence>()
+ verify(sysuiDialog).setMessage(result.capture())
+ assertThat(result.firstValue.toString()).isEqualTo("You will stop sharing Fake Package")
}
@Test
fun negativeButton() {
+ createAndSetDelegate(SINGLE_TASK)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null)
@@ -86,6 +115,8 @@
@Test
fun positiveButton() =
kosmos.testScope.runTest {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
@@ -105,4 +136,20 @@
assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
}
+
+ private fun createAndSetDelegate(state: MediaProjectionState.Projecting) {
+ underTest =
+ EndShareToAppDialogDelegate(
+ kosmos.endMediaProjectionDialogHelper,
+ kosmos.mediaProjectionChipInteractor,
+ state,
+ )
+ }
+
+ companion object {
+ private const val HOST_PACKAGE = "fake.host.package"
+ private val ENTIRE_SCREEN = MediaProjectionState.Projecting.EntireScreen(HOST_PACKAGE)
+ private val SINGLE_TASK =
+ MediaProjectionState.Projecting.SingleTask(HOST_PACKAGE, createTask(taskId = 1))
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 6712963..65bf0bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
@@ -39,8 +40,7 @@
@SmallTest
class OngoingActivityChipsViewModelTest : SysuiTestCase() {
-
- private val kosmos = Kosmos()
+ private val kosmos = Kosmos().also { it.testCase = this }
private val testScope = kosmos.testScope
private val screenRecordState = kosmos.screenRecordRepository.screenRecordState
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
index 057dcb2..6af14e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
@@ -35,6 +35,7 @@
import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator
import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
+import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
@@ -390,7 +391,7 @@
testScope.runTest {
val latest by collectLastValue(underTest.statusBarAppearance)
- ongoingCallRepository.setHasOngoingCall(true)
+ ongoingCallRepository.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 34))
onSystemBarAttributesChanged(
requestedVisibleTypes = WindowInsets.Type.navigationBars(),
)
@@ -403,7 +404,7 @@
testScope.runTest {
val latest by collectLastValue(underTest.statusBarAppearance)
- ongoingCallRepository.setHasOngoingCall(true)
+ ongoingCallRepository.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 789))
onSystemBarAttributesChanged(
requestedVisibleTypes = WindowInsets.Type.statusBars(),
appearance = APPEARANCE_OPAQUE_STATUS_BARS,
@@ -417,7 +418,7 @@
testScope.runTest {
val latest by collectLastValue(underTest.statusBarAppearance)
- ongoingCallRepository.setHasOngoingCall(false)
+ ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall)
onSystemBarAttributesChanged(
requestedVisibleTypes = WindowInsets.Type.navigationBars(),
appearance = APPEARANCE_OPAQUE_STATUS_BARS,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index 4d6798b..feef943 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -32,16 +32,17 @@
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository
import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
-import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository
+import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.util.concurrency.FakeExecutor
@@ -60,13 +61,13 @@
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.nullable
import org.mockito.Mock
-import org.mockito.Mockito.`when`
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
private const val CALL_UID = 900
@@ -93,8 +94,8 @@
private lateinit var controller: OngoingCallController
private lateinit var notifCollectionListener: NotifCollectionListener
- @Mock private lateinit var mockSwipeStatusBarAwayGestureHandler:
- SwipeStatusBarAwayGestureHandler
+ @Mock
+ private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler
@Mock private lateinit var mockOngoingCallListener: OngoingCallListener
@Mock private lateinit var mockActivityStarter: ActivityStarter
@Mock private lateinit var mockIActivityManager: IActivityManager
@@ -112,21 +113,22 @@
MockitoAnnotations.initMocks(this)
val notificationCollection = mock(CommonNotifCollection::class.java)
- controller = OngoingCallController(
- testScope.backgroundScope,
- context,
- ongoingCallRepository,
- notificationCollection,
- clock,
- mockActivityStarter,
- mainExecutor,
- mockIActivityManager,
- OngoingCallLogger(uiEventLoggerFake),
- DumpManager(),
- mockStatusBarWindowController,
- mockSwipeStatusBarAwayGestureHandler,
- statusBarModeRepository,
- )
+ controller =
+ OngoingCallController(
+ testScope.backgroundScope,
+ context,
+ ongoingCallRepository,
+ notificationCollection,
+ clock,
+ mockActivityStarter,
+ mainExecutor,
+ mockIActivityManager,
+ OngoingCallLogger(uiEventLoggerFake),
+ DumpManager(),
+ mockStatusBarWindowController,
+ mockSwipeStatusBarAwayGestureHandler,
+ statusBarModeRepository,
+ )
controller.start()
controller.addCallback(mockOngoingCallListener)
controller.setChipView(chipView)
@@ -136,7 +138,7 @@
notifCollectionListener = collectionListenerCaptor.value!!
`when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
- .thenReturn(PROC_STATE_INVISIBLE)
+ .thenReturn(PROC_STATE_INVISIBLE)
}
@After
@@ -146,10 +148,14 @@
@Test
fun onEntryUpdated_isOngoingCallNotif_listenerAndRepoNotified() {
- notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+ val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
+ notification.modifyNotification(context).setWhen(567)
+ notifCollectionListener.onEntryUpdated(notification.build())
verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
- assertThat(ongoingCallRepository.hasOngoingCall.value).isTrue()
+ val repoState = ongoingCallRepository.ongoingCallState.value
+ assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java)
+ assertThat((repoState as OngoingCallModel.InCall).startTimeMs).isEqualTo(567)
}
@Test
@@ -164,7 +170,8 @@
notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
- assertThat(ongoingCallRepository.hasOngoingCall.value).isFalse()
+ assertThat(ongoingCallRepository.ongoingCallState.value)
+ .isInstanceOf(OngoingCallModel.NoCall::class.java)
}
@Test
@@ -172,25 +179,27 @@
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
- verify(mockOngoingCallListener, times(2))
- .onOngoingCallStateChanged(anyBoolean())
+ verify(mockOngoingCallListener, times(2)).onOngoingCallStateChanged(anyBoolean())
}
@Test
fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_repoUpdated() {
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
- assertThat(ongoingCallRepository.hasOngoingCall.value).isTrue()
+ assertThat(ongoingCallRepository.ongoingCallState.value)
+ .isInstanceOf(OngoingCallModel.InCall::class.java)
notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
- assertThat(ongoingCallRepository.hasOngoingCall.value).isFalse()
+ assertThat(ongoingCallRepository.ongoingCallState.value)
+ .isInstanceOf(OngoingCallModel.NoCall::class.java)
}
/** Regression test for b/191472854. */
@Test
fun onEntryUpdated_notifHasNullContentIntent_noCrash() {
notifCollectionListener.onEntryUpdated(
- createCallNotifEntry(ongoingCallStyle, nullContentIntent = true))
+ createCallNotifEntry(ongoingCallStyle, nullContentIntent = true)
+ )
}
/** Regression test for b/192379214. */
@@ -202,12 +211,12 @@
notifCollectionListener.onEntryUpdated(notification.build())
chipView.measure(
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+ View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
+ View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
- .isEqualTo(0)
+ .isEqualTo(0)
}
@Test
@@ -218,12 +227,12 @@
notifCollectionListener.onEntryUpdated(notification.build())
chipView.measure(
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+ View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
+ View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
- .isGreaterThan(0)
+ .isGreaterThan(0)
}
@Test
@@ -233,12 +242,12 @@
notifCollectionListener.onEntryUpdated(notification.build())
chipView.measure(
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+ View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
+ View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
- .isGreaterThan(0)
+ .isGreaterThan(0)
}
/** Regression test for b/194731244. */
@@ -250,15 +259,14 @@
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
}
- verify(mockIActivityManager, times(1))
- .registerUidObserver(any(), any(), any(), any())
+ verify(mockIActivityManager, times(1)).registerUidObserver(any(), any(), any(), any())
}
/** Regression test for b/216248574. */
@Test
fun entryUpdated_getUidProcessStateThrowsException_noCrash() {
`when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
- .thenThrow(SecurityException())
+ .thenThrow(SecurityException())
// No assert required, just check no crash
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
@@ -267,9 +275,15 @@
/** Regression test for b/216248574. */
@Test
fun entryUpdated_registerUidObserverThrowsException_noCrash() {
- `when`(mockIActivityManager.registerUidObserver(
- any(), any(), any(), nullable(String::class.java)
- )).thenThrow(SecurityException())
+ `when`(
+ mockIActivityManager.registerUidObserver(
+ any(),
+ any(),
+ any(),
+ nullable(String::class.java),
+ )
+ )
+ .thenThrow(SecurityException())
// No assert required, just check no crash
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
@@ -281,9 +295,8 @@
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
val packageNameCaptor = ArgumentCaptor.forClass(String::class.java)
- verify(mockIActivityManager).registerUidObserver(
- any(), any(), any(), packageNameCaptor.capture()
- )
+ verify(mockIActivityManager)
+ .registerUidObserver(any(), any(), any(), packageNameCaptor.capture())
assertThat(packageNameCaptor.value).isNotNull()
}
@@ -313,11 +326,13 @@
fun onEntryRemoved_callNotifAddedThenRemoved_repoUpdated() {
val ongoingCallNotifEntry = createOngoingCallNotifEntry()
notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
- assertThat(ongoingCallRepository.hasOngoingCall.value).isTrue()
+ assertThat(ongoingCallRepository.ongoingCallState.value)
+ .isInstanceOf(OngoingCallModel.InCall::class.java)
notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
- assertThat(ongoingCallRepository.hasOngoingCall.value).isFalse()
+ assertThat(ongoingCallRepository.ongoingCallState.value)
+ .isInstanceOf(OngoingCallModel.NoCall::class.java)
}
@Test
@@ -360,7 +375,8 @@
notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED)
- assertThat(ongoingCallRepository.hasOngoingCall.value).isFalse()
+ assertThat(ongoingCallRepository.ongoingCallState.value)
+ .isInstanceOf(OngoingCallModel.NoCall::class.java)
}
@Test
@@ -379,7 +395,8 @@
notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED)
- assertThat(ongoingCallRepository.hasOngoingCall.value).isTrue()
+ assertThat(ongoingCallRepository.ongoingCallState.value)
+ .isInstanceOf(OngoingCallModel.InCall::class.java)
}
@Test
@@ -404,7 +421,7 @@
@Test
fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() {
`when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
- .thenReturn(PROC_STATE_INVISIBLE)
+ .thenReturn(PROC_STATE_INVISIBLE)
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
@@ -414,7 +431,7 @@
@Test
fun hasOngoingCall_ongoingCallNotifSentButCallAppVisible_returnsFalse() {
`when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
- .thenReturn(PROC_STATE_VISIBLE)
+ .thenReturn(PROC_STATE_VISIBLE)
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
@@ -472,10 +489,8 @@
lateinit var newChipView: View
TestableLooper.get(this).runWithLooper {
- newChipView = LayoutInflater.from(mContext).inflate(
- R.layout.ongoing_activity_chip,
- null
- )
+ newChipView =
+ LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip, null)
}
// Change the chip view associated with the controller.
@@ -488,13 +503,13 @@
fun callProcessChangesToVisible_listenerNotified() {
// Start the call while the process is invisible.
`when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
- .thenReturn(PROC_STATE_INVISIBLE)
+ .thenReturn(PROC_STATE_INVISIBLE)
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
reset(mockOngoingCallListener)
val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java)
- verify(mockIActivityManager).registerUidObserver(
- captor.capture(), any(), any(), nullable(String::class.java))
+ verify(mockIActivityManager)
+ .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java))
val uidObserver = captor.value
// Update the process to visible.
@@ -509,13 +524,13 @@
fun callProcessChangesToInvisible_listenerNotified() {
// Start the call while the process is visible.
`when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
- .thenReturn(PROC_STATE_VISIBLE)
+ .thenReturn(PROC_STATE_VISIBLE)
notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
reset(mockOngoingCallListener)
val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java)
- verify(mockIActivityManager).registerUidObserver(
- captor.capture(), any(), any(), nullable(String::class.java))
+ verify(mockIActivityManager)
+ .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java))
val uidObserver = captor.value
// Update the process to invisible.
@@ -534,7 +549,7 @@
assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
assertThat(uiEventLoggerFake.eventId(0))
- .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_CLICKED.id)
+ .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_CLICKED.id)
}
/** Regression test for b/212467440. */
@@ -556,8 +571,9 @@
assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
assertThat(uiEventLoggerFake.eventId(0))
- .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_VISIBLE.id)
+ .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_VISIBLE.id)
}
+
// Other tests for notifyChipVisibilityChanged are in [OngoingCallLogger], since
// [OngoingCallController.notifyChipVisibilityChanged] just delegates to that class.
@@ -621,8 +637,7 @@
statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
testScope.runCurrent()
- verify(mockSwipeStatusBarAwayGestureHandler)
- .removeOnGestureDetectedCallback(anyString())
+ verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString())
}
@Test
@@ -635,8 +650,7 @@
notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
- verify(mockSwipeStatusBarAwayGestureHandler)
- .removeOnGestureDetectedCallback(anyString())
+ verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString())
}
// TODO(b/195839150): Add test
@@ -675,5 +689,9 @@
private val hangUpIntent = mock(PendingIntent::class.java)
private val ongoingCallStyle = Notification.CallStyle.forOngoingCall(person, hangUpIntent)
-private val screeningCallStyle = Notification.CallStyle.forScreeningCall(
- person, hangUpIntent, /* answerIntent= */ mock(PendingIntent::class.java))
\ No newline at end of file
+private val screeningCallStyle =
+ Notification.CallStyle.forScreeningCall(
+ person,
+ hangUpIntent,
+ /* answerIntent= */ mock(PendingIntent::class.java),
+ )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepositoryTest.kt
index 56aa7d6..73a86a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepositoryTest.kt
@@ -18,6 +18,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -27,12 +28,13 @@
@Test
fun hasOngoingCall_matchesSet() {
- underTest.setHasOngoingCall(true)
+ val inCallModel = OngoingCallModel.InCall(startTimeMs = 654)
+ underTest.setOngoingCallState(inCallModel)
- assertThat(underTest.hasOngoingCall.value).isTrue()
+ assertThat(underTest.ongoingCallState.value).isEqualTo(inCallModel)
- underTest.setHasOngoingCall(false)
+ underTest.setOngoingCallState(OngoingCallModel.NoCall)
- assertThat(underTest.hasOngoingCall.value).isFalse()
+ assertThat(underTest.ongoingCallState.value).isEqualTo(OngoingCallModel.NoCall)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt
index 95b132d..3de50c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt
@@ -19,6 +19,7 @@
import android.os.PersistableBundle
import android.telephony.CarrierConfigManager
import android.telephony.CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL
+import android.telephony.CarrierConfigManager.KEY_SHOW_5G_SLICE_ICON_BOOL
import android.telephony.CarrierConfigManager.KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -53,16 +54,19 @@
fun processNewConfig_updatesAllFlows() {
assertThat(underTest.shouldInflateSignalStrength.value).isFalse()
assertThat(underTest.showOperatorNameInStatusBar.value).isFalse()
+ assertThat(underTest.allowNetworkSliceIndicator.value).isTrue()
underTest.processNewCarrierConfig(
configWithOverrides(
KEY_INFLATE_SIGNAL_STRENGTH_BOOL to true,
KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL to true,
+ KEY_SHOW_5G_SLICE_ICON_BOOL to false,
)
)
assertThat(underTest.shouldInflateSignalStrength.value).isTrue()
assertThat(underTest.showOperatorNameInStatusBar.value).isTrue()
+ assertThat(underTest.allowNetworkSliceIndicator.value).isFalse()
}
@Test
@@ -79,12 +83,14 @@
configWithOverrides(
KEY_INFLATE_SIGNAL_STRENGTH_BOOL to true,
KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL to true,
+ KEY_SHOW_5G_SLICE_ICON_BOOL to true,
)
)
assertThat(underTest.isUsingDefault).isTrue()
assertThat(underTest.shouldInflateSignalStrength.value).isTrue()
assertThat(underTest.showOperatorNameInStatusBar.value).isTrue()
+ assertThat(underTest.allowNetworkSliceIndicator.value).isTrue()
// Process a new config with no keys
underTest.processNewCarrierConfig(PersistableBundle())
@@ -92,6 +98,7 @@
assertThat(underTest.isUsingDefault).isFalse()
assertThat(underTest.shouldInflateSignalStrength.value).isFalse()
assertThat(underTest.showOperatorNameInStatusBar.value).isFalse()
+ assertThat(underTest.allowNetworkSliceIndicator.value).isFalse()
}
companion object {
@@ -105,6 +112,7 @@
PersistableBundle().also {
it.putBoolean(CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false)
it.putBoolean(CarrierConfigManager.KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL, false)
+ it.putBoolean(CarrierConfigManager.KEY_SHOW_5G_SLICE_ICON_BOOL, true)
}
/** Override the default config with the given (key, value) pair */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 3695d8c..6d8bf55 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -25,6 +25,7 @@
import android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WLAN
import android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN
import android.telephony.CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL
+import android.telephony.CarrierConfigManager.KEY_SHOW_5G_SLICE_ICON_BOOL
import android.telephony.NetworkRegistrationInfo
import android.telephony.NetworkRegistrationInfo.DOMAIN_PS
import android.telephony.NetworkRegistrationInfo.REGISTRATION_STATE_DENIED
@@ -1044,6 +1045,24 @@
}
@Test
+ fun allowNetworkSliceIndicator_exposesCarrierConfigValue() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.allowNetworkSliceIndicator)
+
+ systemUiCarrierConfig.processNewCarrierConfig(
+ configWithOverride(KEY_SHOW_5G_SLICE_ICON_BOOL, true)
+ )
+
+ assertThat(latest).isTrue()
+
+ systemUiCarrierConfig.processNewCarrierConfig(
+ configWithOverride(KEY_SHOW_5G_SLICE_ICON_BOOL, false)
+ )
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
fun isAllowedDuringAirplaneMode_alwaysFalse() =
testScope.runTest {
val latest by collectLastValue(underTest.isAllowedDuringAirplaneMode)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index dfe8023..1488418 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -194,6 +194,50 @@
}
@Test
+ fun networkSlice_configOn_hasPrioritizedCaps_showsSlice() =
+ testScope.runTest {
+ connectionRepository.allowNetworkSliceIndicator.value = true
+ val latest by collectLastValue(underTest.showSliceAttribution)
+
+ connectionRepository.hasPrioritizedNetworkCapabilities.value = true
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun networkSlice_configOn_noPrioritizedCaps_noSlice() =
+ testScope.runTest {
+ connectionRepository.allowNetworkSliceIndicator.value = true
+ val latest by collectLastValue(underTest.showSliceAttribution)
+
+ connectionRepository.hasPrioritizedNetworkCapabilities.value = false
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun networkSlice_configOff_hasPrioritizedCaps_noSlice() =
+ testScope.runTest {
+ connectionRepository.allowNetworkSliceIndicator.value = false
+ val latest by collectLastValue(underTest.showSliceAttribution)
+
+ connectionRepository.hasPrioritizedNetworkCapabilities.value = true
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun networkSlice_configOff_noPrioritizedCaps_noSlice() =
+ testScope.runTest {
+ connectionRepository.allowNetworkSliceIndicator.value = false
+ val latest by collectLastValue(underTest.showSliceAttribution)
+
+ connectionRepository.hasPrioritizedNetworkCapabilities.value = false
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
fun iconGroup_three_g() =
testScope.runTest {
connectionRepository.resolvedNetworkType.value =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
index cdb2b88..b8299e5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
@@ -29,6 +29,7 @@
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.assertLogsWtf
@@ -63,7 +64,10 @@
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
class CollapsedStatusBarViewModelImplTest : SysuiTestCase() {
- private val kosmos = Kosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
+ private val kosmos = Kosmos().also {
+ it.testCase = this
+ it.testDispatcher = UnconfinedTestDispatcher()
+ }
private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt
index 062b448..9d22811 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt
@@ -21,7 +21,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
-import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper
import com.android.systemui.util.time.fakeSystemClock
val Kosmos.mediaProjectionChipInteractor: MediaProjectionChipInteractor by
@@ -31,7 +31,7 @@
mediaProjectionRepository = fakeMediaProjectionRepository,
packageManager = packageManager,
systemClock = fakeSystemClock,
- dialogFactory = mockSystemUIDialogFactory,
+ endMediaProjectionDialogHelper = endMediaProjectionDialogHelper,
dialogTransitionAnimator = mockDialogTransitionAnimator,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
new file mode 100644
index 0000000..4f82662
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.mediaprojection.ui.view
+
+import android.content.applicationContext
+import android.content.packageManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+
+val Kosmos.endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper by
+ Kosmos.Fixture {
+ EndMediaProjectionDialogHelper(
+ dialogFactory = mockSystemUIDialogFactory,
+ packageManager = packageManager,
+ context = applicationContext,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index eb2d6c0..c3c3cce 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -32,6 +32,7 @@
) : MobileConnectionRepository {
override val carrierId = MutableStateFlow(UNKNOWN_CARRIER_ID)
override val inflateSignalStrength: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ override val allowNetworkSliceIndicator: MutableStateFlow<Boolean> = MutableStateFlow(true)
override val isEmergencyOnly = MutableStateFlow(false)
override val isRoaming = MutableStateFlow(false)
override val operatorAlphaShort: MutableStateFlow<String?> = MutableStateFlow(null)
diff --git a/proto/src/ondeviceintelligence/inference_info.proto b/proto/src/ondeviceintelligence/inference_info.proto
new file mode 100644
index 0000000..a6f4f4f
--- /dev/null
+++ b/proto/src/ondeviceintelligence/inference_info.proto
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package android.ondeviceintelligence;
+
+option java_package = "com.android.server.ondeviceintelligence";
+option java_multiple_files = true;
+
+
+message InferenceInfo {
+ // Uid for the caller app.
+ optional int32 uid = 1;
+ // Inference start time(milliseconds from the epoch time).
+ optional int64 start_time_ms = 2;
+ // Inference end time(milliseconds from the epoch time).
+ optional int64 end_time_ms = 3;
+ // Suspended time in milliseconds.
+ optional int64 suspended_time_ms = 4;
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index d750065..5ffab55 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -14642,7 +14642,7 @@
final StringBuilder sb = new StringBuilder("registerReceiver: ");
sb.append(Binder.getCallingUid()); sb.append('/');
sb.append(receiverId == null ? "null" : receiverId); sb.append('/');
- final int actionsCount = filter.countActions();
+ final int actionsCount = filter.safeCountActions();
if (actionsCount > 0) {
for (int i = 0; i < actionsCount; ++i) {
sb.append(filter.getAction(i));
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index 9e8bf0e..69452310 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -195,6 +195,18 @@
}
/**
+ * Returns {@link InputMethodInfo} that is queried from {@link #getSelectedMethodId()}.
+ *
+ * @return {@link InputMethodInfo} whose IME ID is the same as {@link #getSelectedMethodId()}.
+ * {@code null} otherwise
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ InputMethodInfo getSelectedMethod() {
+ return InputMethodSettingsRepository.get(mUserId).getMethodMap().get(mSelectedMethodId);
+ }
+
+ /**
* The token we have made for the currently active input method, to
* identify it in the future.
*/
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 223d548..88636a7a 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -4192,10 +4192,10 @@
@GuardedBy("ImfLock.class")
private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme) {
- final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId);
+ final int userId = mCurrentUserId;
+ final var currentImi = getInputMethodBindingController(userId).getSelectedMethod();
final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked(
- onlyCurrentIme, settings.getMethodMap().get(getSelectedMethodIdLocked()),
- mCurrentSubtype);
+ onlyCurrentIme, currentImi, mCurrentSubtype);
if (nextSubtype == null) {
return false;
}
@@ -4210,10 +4210,10 @@
if (!calledWithValidTokenLocked(token)) {
return false;
}
- final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId);
+ final int userId = mCurrentUserId;
+ final var currentImi = getInputMethodBindingController(userId).getSelectedMethod();
final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked(
- false /* onlyCurrentIme */,
- settings.getMethodMap().get(getSelectedMethodIdLocked()), mCurrentSubtype);
+ false /* onlyCurrentIme */, currentImi, mCurrentSubtype);
return nextSubtype != null;
}
}
@@ -4648,8 +4648,7 @@
if (mCurrentUserId != mSwitchingController.getUserId()) {
return;
}
- final InputMethodInfo imi = InputMethodSettingsRepository.get(mCurrentUserId)
- .getMethodMap().get(getSelectedMethodIdLocked());
+ final var imi = getInputMethodBindingController(mCurrentUserId).getSelectedMethod();
if (imi != null) {
mSwitchingController.onUserActionLocked(imi, mCurrentSubtype);
}
@@ -5906,27 +5905,36 @@
synchronized (ImfLock.class) {
final int uid = Binder.getCallingUid();
- if (getSelectedMethodIdLocked() == null) {
+ final int imeUserId = UserHandle.getUserId(uid);
+ if (imeUserId != mCurrentUserId) {
+ // Currently concurrent multi-user is not supported here due to the remaining
+ // dependency on mCurEditorInfo and mCurClient.
+ // TODO(b/341558132): Remove this early-exit once it becomes multi-user ready.
+ Slog.i(TAG, "Ignoring createInputContentUriToken due to user ID mismatch."
+ + " imeUserId=" + imeUserId + " mCurrentUserId=" + mCurrentUserId);
return null;
}
- if (getCurTokenLocked() != token) {
- Slog.e(TAG, "Ignoring createInputContentUriToken mCurToken=" + getCurTokenLocked()
- + " token=" + token);
+ final var bindingController = getInputMethodBindingController(imeUserId);
+ if (bindingController.getSelectedMethodId() == null) {
+ return null;
+ }
+ if (bindingController.getCurToken() != token) {
+ Slog.e(TAG, "Ignoring createInputContentUriToken mCurToken="
+ + bindingController.getCurToken() + " token=" + token);
return null;
}
// 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.
- final var curPackageName = mCurEditorInfo != null
- ? mCurEditorInfo.packageName : null;
+ // TODO(b/341558132): Use "imeUserId" to query per-user "curEditorInfo"
+ final var curPackageName = mCurEditorInfo != null ? mCurEditorInfo.packageName : null;
if (!TextUtils.equals(curPackageName, packageName)) {
Slog.e(TAG, "Ignoring createInputContentUriToken mCurEditorInfo.packageName="
+ curPackageName + " packageName=" + packageName);
return null;
}
- // This user ID can never bee spoofed.
- final int imeUserId = UserHandle.getUserId(uid);
- // This user ID can never bee spoofed.
+ // This user ID can never be spoofed.
+ // TODO(b/341558132): Use "imeUserId" to query per-user "curClient"
final int appUserId = UserHandle.getUserId(mCurClient.mUid);
// This user ID may be invalid if "contentUri" embedded an invalid user ID.
final int contentUriOwnerUserId = ContentProvider.getUserIdFromUri(contentUri,
diff --git a/services/core/java/com/android/server/ondeviceintelligence/BundleUtil.java b/services/core/java/com/android/server/ondeviceintelligence/BundleUtil.java
index 96ab2cc..7dd8f2f 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/BundleUtil.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/BundleUtil.java
@@ -188,7 +188,8 @@
public static IStreamingResponseCallback wrapWithValidation(
IStreamingResponseCallback streamingResponseCallback,
Executor resourceClosingExecutor,
- AndroidFuture future) {
+ AndroidFuture future,
+ InferenceInfoStore inferenceInfoStore) {
return new IStreamingResponseCallback.Stub() {
@Override
public void onNewContent(Bundle processedResult) throws RemoteException {
@@ -207,6 +208,7 @@
sanitizeResponseParams(resultBundle);
streamingResponseCallback.onSuccess(resultBundle);
} finally {
+ inferenceInfoStore.addInferenceInfoFromBundle(resultBundle);
resourceClosingExecutor.execute(() -> tryCloseResource(resultBundle));
future.complete(null);
}
@@ -216,6 +218,7 @@
public void onFailure(int errorCode, String errorMessage,
PersistableBundle errorParams) throws RemoteException {
streamingResponseCallback.onFailure(errorCode, errorMessage, errorParams);
+ inferenceInfoStore.addInferenceInfoFromBundle(errorParams);
future.completeExceptionally(new TimeoutException());
}
@@ -245,7 +248,8 @@
public static IResponseCallback wrapWithValidation(IResponseCallback responseCallback,
Executor resourceClosingExecutor,
- AndroidFuture future) {
+ AndroidFuture future,
+ InferenceInfoStore inferenceInfoStore) {
return new IResponseCallback.Stub() {
@Override
public void onSuccess(Bundle resultBundle)
@@ -254,6 +258,7 @@
sanitizeResponseParams(resultBundle);
responseCallback.onSuccess(resultBundle);
} finally {
+ inferenceInfoStore.addInferenceInfoFromBundle(resultBundle);
resourceClosingExecutor.execute(() -> tryCloseResource(resultBundle));
future.complete(null);
}
@@ -263,6 +268,7 @@
public void onFailure(int errorCode, String errorMessage,
PersistableBundle errorParams) throws RemoteException {
responseCallback.onFailure(errorCode, errorMessage, errorParams);
+ inferenceInfoStore.addInferenceInfoFromBundle(errorParams);
future.completeExceptionally(new TimeoutException());
}
@@ -291,11 +297,13 @@
public static ITokenInfoCallback wrapWithValidation(ITokenInfoCallback responseCallback,
- AndroidFuture future) {
+ AndroidFuture future,
+ InferenceInfoStore inferenceInfoStore) {
return new ITokenInfoCallback.Stub() {
@Override
public void onSuccess(TokenInfo tokenInfo) throws RemoteException {
responseCallback.onSuccess(tokenInfo);
+ inferenceInfoStore.addInferenceInfoFromBundle(tokenInfo.getInfoParams());
future.complete(null);
}
@@ -303,6 +311,7 @@
public void onFailure(int errorCode, String errorMessage, PersistableBundle errorParams)
throws RemoteException {
responseCallback.onFailure(errorCode, errorMessage, errorParams);
+ inferenceInfoStore.addInferenceInfoFromBundle(errorParams);
future.completeExceptionally(new TimeoutException());
}
};
diff --git a/services/core/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java b/services/core/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java
new file mode 100644
index 0000000..6578853
--- /dev/null
+++ b/services/core/java/com/android/server/ondeviceintelligence/InferenceInfoStore.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ondeviceintelligence;
+
+import android.app.ondeviceintelligence.InferenceInfo;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService;
+import android.util.Slog;
+
+import java.io.IOException;
+import java.util.Base64;
+import java.util.Comparator;
+import java.util.List;
+import java.util.TreeSet;
+
+public class InferenceInfoStore {
+ private static final String TAG = "InferenceInfoStore";
+ private final TreeSet<InferenceInfo> inferenceInfos;
+ private final long maxAgeMs;
+
+ public InferenceInfoStore(long maxAgeMs) {
+ this.maxAgeMs = maxAgeMs;
+ this.inferenceInfos = new TreeSet<>(
+ Comparator.comparingLong(InferenceInfo::getStartTimeMs));
+ }
+
+ public List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) {
+ return inferenceInfos.stream().filter(
+ info -> info.getStartTimeMs() > startTimeEpochMillis).toList();
+ }
+
+ public void addInferenceInfoFromBundle(PersistableBundle pb) {
+ if (!pb.containsKey(OnDeviceSandboxedInferenceService.INFERENCE_INFO_BUNDLE_KEY)) {
+ return;
+ }
+
+ try {
+ String infoBytesBase64String = pb.getString(
+ OnDeviceSandboxedInferenceService.INFERENCE_INFO_BUNDLE_KEY);
+ if (infoBytesBase64String != null) {
+ byte[] infoBytes = Base64.getDecoder().decode(infoBytesBase64String);
+ com.android.server.ondeviceintelligence.nano.InferenceInfo inferenceInfo =
+ com.android.server.ondeviceintelligence.nano.InferenceInfo.parseFrom(
+ infoBytes);
+ add(inferenceInfo);
+ }
+ } catch (IOException e) {
+ Slog.e(TAG, "Unable to parse InferenceInfo from the received bytes.");
+ }
+ }
+
+ public void addInferenceInfoFromBundle(Bundle b) {
+ if (!b.containsKey(OnDeviceSandboxedInferenceService.INFERENCE_INFO_BUNDLE_KEY)) {
+ return;
+ }
+
+ try {
+ byte[] infoBytes = b.getByteArray(
+ OnDeviceSandboxedInferenceService.INFERENCE_INFO_BUNDLE_KEY);
+ if (infoBytes != null) {
+ com.android.server.ondeviceintelligence.nano.InferenceInfo inferenceInfo =
+ com.android.server.ondeviceintelligence.nano.InferenceInfo.parseFrom(
+ infoBytes);
+ add(inferenceInfo);
+ }
+ } catch (IOException e) {
+ Slog.e(TAG, "Unable to parse InferenceInfo from the received bytes.");
+ }
+ }
+
+ private synchronized void add(com.android.server.ondeviceintelligence.nano.InferenceInfo info) {
+ while (System.currentTimeMillis() - inferenceInfos.first().getStartTimeMs() > maxAgeMs) {
+ inferenceInfos.pollFirst();
+ }
+ inferenceInfos.add(toInferenceInfo(info));
+ }
+
+ private static InferenceInfo toInferenceInfo(
+ com.android.server.ondeviceintelligence.nano.InferenceInfo info) {
+ return new InferenceInfo.Builder().setUid(info.uid).setStartTimeMs(
+ info.startTimeMs).setEndTimeMs(info.endTimeMs).setSuspendedTimeMs(
+ info.suspendedTimeMs).build();
+ }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerInternal.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerInternal.java
index 07af8d0..1450dc0 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerInternal.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerInternal.java
@@ -17,5 +17,10 @@
package com.android.server.ondeviceintelligence;
public interface OnDeviceIntelligenceManagerInternal {
+ /**
+ * Gets the uid for the process that is currently hosting the
+ * {@link android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService} registered on
+ * the device.
+ */
int getInferenceServiceUid();
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index 1a43bc2..9ef2e12 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -44,6 +44,7 @@
import android.app.ondeviceintelligence.IResponseCallback;
import android.app.ondeviceintelligence.IStreamingResponseCallback;
import android.app.ondeviceintelligence.ITokenInfoCallback;
+import android.app.ondeviceintelligence.InferenceInfo;
import android.app.ondeviceintelligence.OnDeviceIntelligenceException;
import android.content.ComponentName;
import android.content.Context;
@@ -127,6 +128,7 @@
private static final String NAMESPACE_ON_DEVICE_INTELLIGENCE = "ondeviceintelligence";
private static final String SYSTEM_PACKAGE = "android";
+ private static final long MAX_AGE_MS = TimeUnit.HOURS.toMillis(3);
private final Executor resourceClosingExecutor = Executors.newCachedThreadPool();
@@ -138,7 +140,7 @@
private final Context mContext;
protected final Object mLock = new Object();
-
+ private final InferenceInfoStore mInferenceInfoStore;
private RemoteOnDeviceSandboxedInferenceService mRemoteInferenceService;
private RemoteOnDeviceIntelligenceService mRemoteOnDeviceIntelligenceService;
volatile boolean mIsServiceEnabled;
@@ -170,6 +172,7 @@
super(context);
mContext = context;
mTemporaryServiceNames = new String[0];
+ mInferenceInfoStore = new InferenceInfoStore(MAX_AGE_MS);
}
@Override
@@ -223,6 +226,14 @@
}
@Override
+ public List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) {
+ mContext.enforceCallingPermission(
+ Manifest.permission.DUMP, TAG);
+ return OnDeviceIntelligenceManagerService.this.getLatestInferenceInfo(
+ startTimeEpochMillis);
+ }
+
+ @Override
public void getVersion(RemoteCallback remoteCallback) {
Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getVersion");
Objects.requireNonNull(remoteCallback);
@@ -434,7 +445,8 @@
service.requestTokenInfo(callerUid, feature,
request,
wrapCancellationFuture(cancellationSignalFuture),
- wrapWithValidation(tokenInfoCallback, future));
+ wrapWithValidation(tokenInfoCallback, future,
+ mInferenceInfoStore));
return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS);
});
result.whenCompleteAsync((c, e) -> BundleUtil.tryCloseResource(request),
@@ -480,7 +492,8 @@
wrapCancellationFuture(cancellationSignalFuture),
wrapProcessingFuture(processingSignalFuture),
wrapWithValidation(responseCallback,
- resourceClosingExecutor, future));
+ resourceClosingExecutor, future,
+ mInferenceInfoStore));
return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS);
});
result.whenCompleteAsync((c, e) -> BundleUtil.tryCloseResource(request),
@@ -525,7 +538,8 @@
wrapCancellationFuture(cancellationSignalFuture),
wrapProcessingFuture(processingSignalFuture),
wrapWithValidation(streamingCallback,
- resourceClosingExecutor, future));
+ resourceClosingExecutor, future,
+ mInferenceInfoStore));
return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS);
});
result.whenCompleteAsync((c, e) -> BundleUtil.tryCloseResource(request),
@@ -846,6 +860,10 @@
&& (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0;
}
+ private List<InferenceInfo> getLatestInferenceInfo(long startTimeEpochMillis) {
+ return mInferenceInfoStore.getLatestInferenceInfo(startTimeEpochMillis);
+ }
+
@Nullable
public String getRemoteConfiguredPackageName() {
try {
@@ -1066,7 +1084,7 @@
}
private void setRemoteInferenceServiceUid(int remoteInferenceServiceUid) {
- synchronized (mLock){
+ synchronized (mLock) {
this.remoteInferenceServiceUid = remoteInferenceServiceUid;
}
}
diff --git a/tests/TouchLatency/app/src/main/res/values/styles.xml b/tests/TouchLatency/app/src/main/res/values/styles.xml
index fa352cf..5058331 100644
--- a/tests/TouchLatency/app/src/main/res/values/styles.xml
+++ b/tests/TouchLatency/app/src/main/res/values/styles.xml
@@ -18,7 +18,7 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
- <item name="android:windowLayoutInDisplayCutoutMode">default</item>
+ <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
</resources>