Merge "Setting for Lockscreen Clock" into tm-qpr-dev
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 0de0a1c..e8bcfd2 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -112,6 +112,7 @@
import android.text.format.DateFormat;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.EventLog;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.LongArrayQueue;
@@ -334,12 +335,18 @@
"REORDER_ALARMS_FOR_TARE",
});
- BroadcastOptions mOptsWithFgs = BroadcastOptions.makeBasic();
- BroadcastOptions mOptsWithFgsForAlarmClock = BroadcastOptions.makeBasic();
- BroadcastOptions mOptsWithoutFgs = BroadcastOptions.makeBasic();
- BroadcastOptions mOptsTimeBroadcast = BroadcastOptions.makeBasic();
+ BroadcastOptions mOptsWithFgs = makeBasicAlarmBroadcastOptions();
+ BroadcastOptions mOptsWithFgsForAlarmClock = makeBasicAlarmBroadcastOptions();
+ BroadcastOptions mOptsWithoutFgs = makeBasicAlarmBroadcastOptions();
+ BroadcastOptions mOptsTimeBroadcast = makeBasicAlarmBroadcastOptions();
ActivityOptions mActivityOptsRestrictBal = ActivityOptions.makeBasic();
- BroadcastOptions mBroadcastOptsRestrictBal = BroadcastOptions.makeBasic();
+ BroadcastOptions mBroadcastOptsRestrictBal = makeBasicAlarmBroadcastOptions();
+
+ private static BroadcastOptions makeBasicAlarmBroadcastOptions() {
+ final BroadcastOptions b = BroadcastOptions.makeBasic();
+ b.setAlarmBroadcast(true);
+ return b;
+ }
// TODO(b/172085676): Move inside alarm store.
private final SparseArray<AlarmManager.AlarmClockInfo> mNextAlarmClockForUser =
@@ -2299,7 +2306,11 @@
+ " reached for uid: " + UserHandle.formatUid(callingUid)
+ ", callingPackage: " + callingPackage;
Slog.w(TAG, errorMsg);
- throw new IllegalStateException(errorMsg);
+ if (callingUid != Process.SYSTEM_UID) {
+ throw new IllegalStateException(errorMsg);
+ } else {
+ EventLog.writeEvent(0x534e4554, "234441463", -1, errorMsg);
+ }
}
setImplLocked(type, triggerAtTime, triggerElapsed, windowLength, interval, operation,
directReceiver, listenerTag, flags, workSource, alarmClock, callingUid,
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ec4ad8b..0126199 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -5956,7 +5956,7 @@
method public int getEphemerisSource();
method @FloatRange public double getIonoDelayMeters();
method @IntRange(from=0, to=1023) public int getIssueOfDataClock();
- method @IntRange(from=0, to=255) public int getIssueOfDataEphemeris();
+ method @IntRange(from=0, to=1023) public int getIssueOfDataEphemeris();
method @Nullable public android.location.SatellitePvt.PositionEcef getPositionEcef();
method @IntRange(from=0) public long getTimeOfClockSeconds();
method @IntRange(from=0) public long getTimeOfEphemerisSeconds();
@@ -5984,7 +5984,7 @@
method @NonNull public android.location.SatellitePvt.Builder setEphemerisSource(int);
method @NonNull public android.location.SatellitePvt.Builder setIonoDelayMeters(@FloatRange(from=0.0f, to=100.0f) double);
method @NonNull public android.location.SatellitePvt.Builder setIssueOfDataClock(@IntRange(from=0, to=1023) int);
- method @NonNull public android.location.SatellitePvt.Builder setIssueOfDataEphemeris(@IntRange(from=0, to=255) int);
+ method @NonNull public android.location.SatellitePvt.Builder setIssueOfDataEphemeris(@IntRange(from=0, to=1023) int);
method @NonNull public android.location.SatellitePvt.Builder setPositionEcef(@NonNull android.location.SatellitePvt.PositionEcef);
method @NonNull public android.location.SatellitePvt.Builder setTimeOfClockSeconds(@IntRange(from=0) long);
method @NonNull public android.location.SatellitePvt.Builder setTimeOfEphemerisSeconds(@IntRange(from=0) long);
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index 56f8760..f0e1448 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -53,6 +53,7 @@
private String[] mRequireNoneOfPermissions;
private long mRequireCompatChangeId = CHANGE_INVALID;
private boolean mRequireCompatChangeEnabled = true;
+ private boolean mIsAlarmBroadcast = false;
private long mIdForResponseEvent;
/**
@@ -149,6 +150,13 @@
"android:broadcast.requireCompatChangeEnabled";
/**
+ * Corresponds to {@link #setAlarmBroadcast(boolean)}
+ * @hide
+ */
+ public static final String KEY_ALARM_BROADCAST =
+ "android:broadcast.is_alarm";
+
+ /**
* @hide
* @deprecated Use {@link android.os.PowerExemptionManager#
* TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED} instead.
@@ -207,6 +215,7 @@
mRequireCompatChangeId = opts.getLong(KEY_REQUIRE_COMPAT_CHANGE_ID, CHANGE_INVALID);
mRequireCompatChangeEnabled = opts.getBoolean(KEY_REQUIRE_COMPAT_CHANGE_ENABLED, true);
mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT);
+ mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false);
}
/**
@@ -498,6 +507,27 @@
mRequireCompatChangeEnabled = true;
}
+ /**
+ * When set, this broadcast will be understood as having originated from an
+ * alarm going off. Only the OS itself can use this option; uses by other
+ * senders will be ignored.
+ * @hide
+ *
+ * @param senderIsAlarm Whether the broadcast is alarm-triggered.
+ */
+ public void setAlarmBroadcast(boolean senderIsAlarm) {
+ mIsAlarmBroadcast = senderIsAlarm;
+ }
+
+ /**
+ * Did this broadcast originate from an alarm triggering?
+ * @return true if this broadcast is an alarm message, false otherwise
+ * @hide
+ */
+ public boolean isAlarmBroadcast() {
+ return mIsAlarmBroadcast;
+ }
+
/** {@hide} */
public long getRequireCompatChangeId() {
return mRequireCompatChangeId;
@@ -560,6 +590,9 @@
b.putInt(KEY_TEMPORARY_APP_ALLOWLIST_REASON_CODE, mTemporaryAppAllowlistReasonCode);
b.putString(KEY_TEMPORARY_APP_ALLOWLIST_REASON, mTemporaryAppAllowlistReason);
}
+ if (mIsAlarmBroadcast) {
+ b.putBoolean(KEY_ALARM_BROADCAST, true);
+ }
if (mMinManifestReceiverApiLevel != 0) {
b.putInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, mMinManifestReceiverApiLevel);
}
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 8984c42..556058b 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -783,6 +783,17 @@
return null;
}
+ /**
+ * This is called after starting an Activity and provides the result code that defined in
+ * {@link ActivityManager}, like {@link ActivityManager#START_SUCCESS}.
+ *
+ * @param result the result code that returns after starting an Activity.
+ * @param bOptions the bundle generated from {@link ActivityOptions} that originally
+ * being used to start the Activity.
+ * @hide
+ */
+ public void onStartActivityResult(int result, @NonNull Bundle bOptions) {}
+
final boolean match(Context who,
Activity activity,
Intent intent) {
@@ -1344,6 +1355,28 @@
return apk.getAppFactory();
}
+ /**
+ * This should be called before {@link #checkStartActivityResult(int, Object)}, because
+ * exceptions might be thrown while checking the results.
+ */
+ private void notifyStartActivityResult(int result, @Nullable Bundle options) {
+ if (mActivityMonitors == null) {
+ return;
+ }
+ synchronized (mSync) {
+ final int size = mActivityMonitors.size();
+ for (int i = 0; i < size; i++) {
+ final ActivityMonitor am = mActivityMonitors.get(i);
+ if (am.ignoreMatchingSpecificIntents()) {
+ if (options == null) {
+ options = ActivityOptions.makeBasic().toBundle();
+ }
+ am.onStartActivityResult(result, options);
+ }
+ }
+ }
+ }
+
private void prePerformCreate(Activity activity) {
if (mWaitingActivities != null) {
synchronized (mSync) {
@@ -1802,6 +1835,7 @@
who.getOpPackageName(), who.getAttributionTag(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()), token,
target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
+ notifyStartActivityResult(result, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
@@ -1876,6 +1910,7 @@
int result = ActivityTaskManager.getService().startActivities(whoThread,
who.getOpPackageName(), who.getAttributionTag(), intents, resolvedTypes,
token, options, userId);
+ notifyStartActivityResult(result, options);
checkStartActivityResult(result, intents[0]);
return result;
} catch (RemoteException e) {
@@ -1947,6 +1982,7 @@
who.getOpPackageName(), who.getAttributionTag(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()), token, target,
requestCode, 0, null, options);
+ notifyStartActivityResult(result, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
@@ -2017,6 +2053,7 @@
who.getOpPackageName(), who.getAttributionTag(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()), token, resultWho,
requestCode, 0, null, options, user.getIdentifier());
+ notifyStartActivityResult(result, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
@@ -2068,6 +2105,7 @@
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options,
ignoreTargetSecurity, userId);
+ notifyStartActivityResult(result, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
@@ -2115,6 +2153,7 @@
int result = appTask.startActivity(whoThread.asBinder(), who.getOpPackageName(),
who.getAttributionTag(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()), options);
+ notifyStartActivityResult(result, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableArray.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableArray.java
index 6cf5d60..cbd8066 100644
--- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableArray.java
+++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableArray.java
@@ -45,46 +45,41 @@
private static final boolean DEBUG = false;
private static interface PrimitiveArrayFiller {
- public void fillPosition(Object arr, int index, ByteBuffer buffer);
+ public void fillArray(Object arr, int size, ByteBuffer buffer);
static PrimitiveArrayFiller getPrimitiveArrayFiller(Class<?> componentType) {
if (componentType == int.class) {
return new PrimitiveArrayFiller() {
@Override
- public void fillPosition(Object arr, int index, ByteBuffer buffer) {
- int i = buffer.getInt();
- Array.setInt(arr, index, i);
+ public void fillArray(Object arr, int size, ByteBuffer buffer) {
+ buffer.asIntBuffer().get(int[].class.cast(arr), 0, size);
}
};
} else if (componentType == float.class) {
return new PrimitiveArrayFiller() {
@Override
- public void fillPosition(Object arr, int index, ByteBuffer buffer) {
- float i = buffer.getFloat();
- Array.setFloat(arr, index, i);
+ public void fillArray(Object arr, int size, ByteBuffer buffer) {
+ buffer.asFloatBuffer().get(float[].class.cast(arr), 0, size);
}
};
} else if (componentType == long.class) {
return new PrimitiveArrayFiller() {
@Override
- public void fillPosition(Object arr, int index, ByteBuffer buffer) {
- long i = buffer.getLong();
- Array.setLong(arr, index, i);
+ public void fillArray(Object arr, int size, ByteBuffer buffer) {
+ buffer.asLongBuffer().get(long[].class.cast(arr), 0, size);
}
};
} else if (componentType == double.class) {
return new PrimitiveArrayFiller() {
@Override
- public void fillPosition(Object arr, int index, ByteBuffer buffer) {
- double i = buffer.getDouble();
- Array.setDouble(arr, index, i);
+ public void fillArray(Object arr, int size, ByteBuffer buffer) {
+ buffer.asDoubleBuffer().get(double[].class.cast(arr), 0, size);
}
};
} else if (componentType == byte.class) {
return new PrimitiveArrayFiller() {
@Override
- public void fillPosition(Object arr, int index, ByteBuffer buffer) {
- byte i = buffer.get();
- Array.setByte(arr, index, i);
+ public void fillArray(Object arr, int size, ByteBuffer buffer) {
+ buffer.get(byte[].class.cast(arr), 0, size);
}
};
}
@@ -93,13 +88,6 @@
}
};
- static void unmarshalPrimitiveArray(Object arr, int size, ByteBuffer buffer,
- PrimitiveArrayFiller filler) {
- for (int i = 0; i < size; i++) {
- filler.fillPosition(arr, i, buffer);
- }
- }
-
private class MarshalerArray extends Marshaler<T> {
private final Class<T> mClass;
private final Marshaler<?> mComponentMarshaler;
@@ -150,8 +138,8 @@
array = Array.newInstance(mComponentClass, arraySize);
if (isUnwrappedPrimitiveClass(mComponentClass) &&
mComponentClass == getPrimitiveTypeClass(mNativeType)) {
- unmarshalPrimitiveArray(array, arraySize, buffer,
- PrimitiveArrayFiller.getPrimitiveArrayFiller(mComponentClass));
+ PrimitiveArrayFiller.getPrimitiveArrayFiller(mComponentClass).fillArray(array,
+ arraySize, buffer);
} else {
for (int i = 0; i < arraySize; ++i) {
Object elem = mComponentMarshaler.unmarshal(buffer);
diff --git a/core/java/android/view/IDisplayWindowInsetsController.aidl b/core/java/android/view/IDisplayWindowInsetsController.aidl
index f4a0dfa..1940042 100644
--- a/core/java/android/view/IDisplayWindowInsetsController.aidl
+++ b/core/java/android/view/IDisplayWindowInsetsController.aidl
@@ -16,6 +16,7 @@
package android.view;
+import android.content.ComponentName;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.InsetsVisibilities;
@@ -30,10 +31,11 @@
/**
* Called when top focused window changes to determine whether or not to take over insets
* control. Won't be called if config_remoteInsetsControllerControlsSystemBars is false.
- * @param packageName: Passes the top package name
+ * @param component: Passes the top application component in the focused window.
* @param requestedVisibilities The insets visibilities requested by the focussed window.
*/
- void topFocusedWindowChanged(String packageName, in InsetsVisibilities insetsVisibilities);
+ void topFocusedWindowChanged(in ComponentName component,
+ in InsetsVisibilities insetsVisibilities);
/**
* @see IWindow#insetsChanged
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 2f79cae..da9fd0c2 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -16,6 +16,7 @@
package androidx.window.extensions.embedding;
+import static android.app.ActivityManager.START_SUCCESS;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -97,6 +98,7 @@
private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>();
private final Handler mHandler;
private final Object mLock = new Object();
+ private final ActivityStartMonitor mActivityStartMonitor;
public SplitController() {
final MainThreadExecutor executor = new MainThreadExecutor();
@@ -108,7 +110,8 @@
new LifecycleCallbacks());
// Intercept activity starts to route activities to new containers if necessary.
Instrumentation instrumentation = activityThread.getInstrumentation();
- instrumentation.addMonitor(new ActivityStartMonitor());
+ mActivityStartMonitor = new ActivityStartMonitor();
+ instrumentation.addMonitor(mActivityStartMonitor);
}
/** Updates the embedding rules applied to future activity launches. */
@@ -1385,6 +1388,11 @@
return ActivityThread.currentActivityThread().getActivity(activityToken);
}
+ @VisibleForTesting
+ ActivityStartMonitor getActivityStartMonitor() {
+ return mActivityStartMonitor;
+ }
+
/**
* Gets the token of the initial TaskFragment that embedded this activity. Do not rely on it
* after creation because the activity could be reparented.
@@ -1536,7 +1544,10 @@
* A monitor that intercepts all activity start requests originating in the client process and
* can amend them to target a specific task fragment to form a split.
*/
- private class ActivityStartMonitor extends Instrumentation.ActivityMonitor {
+ @VisibleForTesting
+ class ActivityStartMonitor extends Instrumentation.ActivityMonitor {
+ @VisibleForTesting
+ Intent mCurrentIntent;
@Override
public Instrumentation.ActivityResult onStartActivity(@NonNull Context who,
@@ -1564,11 +1575,29 @@
// the dedicated container.
options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
launchedInTaskFragment.getTaskFragmentToken());
+ mCurrentIntent = intent;
}
}
return super.onStartActivity(who, intent, options);
}
+
+ @Override
+ public void onStartActivityResult(int result, @NonNull Bundle bOptions) {
+ super.onStartActivityResult(result, bOptions);
+ if (mCurrentIntent != null && result != START_SUCCESS) {
+ // Clear the pending appeared intent if the activity was not started successfully.
+ final IBinder token = bOptions.getBinder(
+ ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN);
+ if (token != null) {
+ final TaskFragmentContainer container = getContainer(token);
+ if (container != null) {
+ container.clearPendingAppearedIntentIfNeeded(mCurrentIntent);
+ }
+ }
+ }
+ mCurrentIntent = null;
+ }
}
/**
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index abf32a2..a188e2b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -198,6 +198,22 @@
return mPendingAppearedIntent;
}
+ void setPendingAppearedIntent(@Nullable Intent intent) {
+ mPendingAppearedIntent = intent;
+ }
+
+ /**
+ * Clears the pending appeared Intent if it is the same as given Intent. Otherwise, the
+ * pending appeared Intent is cleared when TaskFragmentInfo is set and is not empty (has
+ * running activities).
+ */
+ void clearPendingAppearedIntentIfNeeded(@NonNull Intent intent) {
+ if (mPendingAppearedIntent == null || mPendingAppearedIntent != intent) {
+ return;
+ }
+ mPendingAppearedIntent = null;
+ }
+
boolean hasActivity(@NonNull IBinder token) {
if (mInfo != null && mInfo.getActivities().contains(token)) {
return true;
@@ -230,13 +246,18 @@
void setInfo(@NonNull TaskFragmentInfo info) {
if (!mIsFinished && mInfo == null && info.isEmpty()) {
- // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if it is
- // still empty after timeout.
+ // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if no
+ // pending appeared intent/activities. Otherwise, wait and removing the TaskFragment if
+ // it is still empty after timeout.
mAppearEmptyTimeout = () -> {
mAppearEmptyTimeout = null;
mController.onTaskFragmentAppearEmptyTimeout(this);
};
- mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS);
+ if (mPendingAppearedIntent != null || !mPendingAppearedActivities.isEmpty()) {
+ mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS);
+ } else {
+ mAppearEmptyTimeout.run();
+ }
} else if (mAppearEmptyTimeout != null && !info.isEmpty()) {
mController.getHandler().removeCallbacks(mAppearEmptyTimeout);
mAppearEmptyTimeout = null;
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 042547f..4bc5033 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -16,6 +16,7 @@
package androidx.window.extensions.embedding;
+import static android.app.ActivityManager.START_CANCELED;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -293,6 +294,26 @@
}
@Test
+ public void testOnStartActivityResultError() {
+ final Intent intent = new Intent();
+ final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+ final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+ intent, taskContainer, mSplitController);
+ final SplitController.ActivityStartMonitor monitor =
+ mSplitController.getActivityStartMonitor();
+
+ container.setPendingAppearedIntent(intent);
+ final Bundle bundle = new Bundle();
+ bundle.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
+ container.getTaskFragmentToken());
+ monitor.mCurrentIntent = intent;
+ doReturn(container).when(mSplitController).getContainer(any());
+
+ monitor.onStartActivityResult(START_CANCELED, bundle);
+ assertNull(container.getPendingAppearedIntent());
+ }
+
+ @Test
public void testOnActivityCreated() {
mSplitController.onActivityCreated(mActivity);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 28c2773..44c7e6c 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -209,21 +209,21 @@
assertNull(container.mAppearEmptyTimeout);
- // Not set if it is not appeared empty.
- final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
- doReturn(new ArrayList<>()).when(info).getActivities();
- doReturn(false).when(info).isEmpty();
- container.setInfo(info);
-
- assertNull(container.mAppearEmptyTimeout);
-
// Set timeout if the first info set is empty.
+ final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
container.mInfo = null;
doReturn(true).when(info).isEmpty();
container.setInfo(info);
assertNotNull(container.mAppearEmptyTimeout);
+ // Not set if it is not appeared empty.
+ doReturn(new ArrayList<>()).when(info).getActivities();
+ doReturn(false).when(info).isEmpty();
+ container.setInfo(info);
+
+ assertNull(container.mAppearEmptyTimeout);
+
// Remove timeout after the container becomes non-empty.
doReturn(false).when(info).isEmpty();
container.setInfo(info);
@@ -232,6 +232,7 @@
// Running the timeout will call into SplitController.onTaskFragmentAppearEmptyTimeout.
container.mInfo = null;
+ container.setPendingAppearedIntent(mIntent);
doReturn(true).when(info).isEmpty();
container.setInfo(info);
container.mAppearEmptyTimeout.run();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 6a2acf4..b3f6247 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -20,6 +20,7 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
+import android.content.ComponentName;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
@@ -324,7 +325,7 @@
}
@Override
- public void topFocusedWindowChanged(String packageName,
+ public void topFocusedWindowChanged(ComponentName component,
InsetsVisibilities requestedVisibilities) {
// Do nothing
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
index b670544..f546f11 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.common;
+import android.content.ComponentName;
import android.os.RemoteException;
import android.util.Slog;
import android.util.SparseArray;
@@ -171,14 +172,14 @@
}
}
- private void topFocusedWindowChanged(String packageName,
+ private void topFocusedWindowChanged(ComponentName component,
InsetsVisibilities requestedVisibilities) {
CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
if (listeners == null) {
return;
}
for (OnInsetsChangedListener listener : listeners) {
- listener.topFocusedWindowChanged(packageName, requestedVisibilities);
+ listener.topFocusedWindowChanged(component, requestedVisibilities);
}
}
@@ -186,10 +187,10 @@
private class DisplayWindowInsetsControllerImpl
extends IDisplayWindowInsetsController.Stub {
@Override
- public void topFocusedWindowChanged(String packageName,
+ public void topFocusedWindowChanged(ComponentName component,
InsetsVisibilities requestedVisibilities) throws RemoteException {
mMainExecutor.execute(() -> {
- PerDisplay.this.topFocusedWindowChanged(packageName, requestedVisibilities);
+ PerDisplay.this.topFocusedWindowChanged(component, requestedVisibilities);
});
}
@@ -234,10 +235,10 @@
/**
* Called when top focused window changes to determine whether or not to take over insets
* control. Won't be called if config_remoteInsetsControllerControlsSystemBars is false.
- * @param packageName The name of the package that is open in the top focussed window.
+ * @param component The application component that is open in the top focussed window.
* @param requestedVisibilities The insets visibilities requested by the focussed window.
*/
- default void topFocusedWindowChanged(String packageName,
+ default void topFocusedWindowChanged(ComponentName component,
InsetsVisibilities requestedVisibilities) {}
/**
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
index 3ef3a1f..4a7fd3d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
@@ -24,6 +24,7 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.content.ComponentName;
import android.os.RemoteException;
import android.util.SparseArray;
import android.view.IDisplayWindowInsetsController;
@@ -165,7 +166,7 @@
int hideInsetsCount = 0;
@Override
- public void topFocusedWindowChanged(String packageName,
+ public void topFocusedWindowChanged(ComponentName component,
InsetsVisibilities requestedVisibilities) {
topFocusedWindowChangedCount++;
}
diff --git a/location/java/android/location/SatellitePvt.java b/location/java/android/location/SatellitePvt.java
index f3e1508..2031929 100644
--- a/location/java/android/location/SatellitePvt.java
+++ b/location/java/android/location/SatellitePvt.java
@@ -539,7 +539,7 @@
*
* <p>This field is valid if {@link #hasIssueOfDataEphemeris()} is true.
*/
- @IntRange(from = 0, to = 255)
+ @IntRange(from = 0, to = 1023)
public int getIssueOfDataEphemeris() {
return mIssueOfDataEphemeris;
}
@@ -847,8 +847,8 @@
*/
@NonNull
public Builder setIssueOfDataEphemeris(
- @IntRange(from = 0, to = 255) int issueOfDataEphemeris) {
- Preconditions.checkArgumentInRange(issueOfDataEphemeris, 0, 255,
+ @IntRange(from = 0, to = 1023) int issueOfDataEphemeris) {
+ Preconditions.checkArgumentInRange(issueOfDataEphemeris, 0, 1023,
"issueOfDataEphemeris");
mIssueOfDataEphemeris = issueOfDataEphemeris;
mFlags = (byte) (mFlags | HAS_ISSUE_OF_DATA_EPHEMERIS);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index fa87de2..ffd6b52 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -110,6 +110,7 @@
"androidx.arch.core_core-runtime",
"androidx.lifecycle_lifecycle-common-java8",
"androidx.lifecycle_lifecycle-extensions",
+ "androidx.lifecycle_lifecycle-runtime-ktx",
"androidx.dynamicanimation_dynamicanimation",
"androidx-constraintlayout_constraintlayout",
"androidx.exifinterface_exifinterface",
@@ -218,6 +219,7 @@
"androidx.arch.core_core-runtime",
"androidx.lifecycle_lifecycle-common-java8",
"androidx.lifecycle_lifecycle-extensions",
+ "androidx.lifecycle_lifecycle-runtime-ktx",
"androidx.dynamicanimation_dynamicanimation",
"androidx-constraintlayout_constraintlayout",
"androidx.exifinterface_exifinterface",
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index f9a9ef6..154a6fc 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -102,7 +102,7 @@
//
// If you don't use @Staging or @Postsubmit, your new test will immediately
// block presubmit, which is probably not what you want!
- "platinum-postsubmit": [
+ "sysui-platinum-postsubmit": [
{
"name": "PlatformScenarioTests",
"options": [
@@ -121,7 +121,7 @@
]
}
],
- "staged-platinum-postsubmit": [
+ "sysui-staged-platinum-postsubmit": [
{
"name": "PlatformScenarioTests",
"options": [
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index fbe3356..8ddd430 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -523,9 +523,10 @@
state: LaunchAnimator.State,
linearProgress: Float,
) {
- if (transactionApplierView.viewRootImpl == null) {
- // If the view root we synchronize with was detached, don't apply any transaction
- // (as [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw).
+ if (transactionApplierView.viewRootImpl == null || !window.leash.isValid) {
+ // Don't apply any transaction if the view root we synchronize with was detached or
+ // if the SurfaceControl associated with [window] is not valid, as
+ // [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw.
return
}
@@ -605,9 +606,10 @@
state: LaunchAnimator.State,
linearProgress: Float
) {
- if (transactionApplierView.viewRootImpl == null) {
- // If the view root we synchronize with was detached, don't apply any transaction
- // (as [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw).
+ if (transactionApplierView.viewRootImpl == null || !navigationBar.leash.isValid) {
+ // Don't apply any transaction if the view root we synchronize with was detached or
+ // if the SurfaceControl associated with [navigationBar] is not valid, as
+ // [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw.
return
}
diff --git a/packages/SystemUI/res/drawable/ic_media_pause_container.xml b/packages/SystemUI/res/drawable/ic_media_pause_container.xml
index b92e635..ea9eb8c 100644
--- a/packages/SystemUI/res/drawable/ic_media_pause_container.xml
+++ b/packages/SystemUI/res/drawable/ic_media_pause_container.xml
@@ -22,11 +22,6 @@
android:viewportHeight="48"
android:viewportWidth="48">
<group android:name="_R_G">
- <group android:name="_R_G_L_1_G"
- android:translateX="24"
- android:translateY="24"
- android:scaleX="0.5"
- android:scaleY="0.5"/>
<group android:name="_R_G_L_0_G"
android:translateX="24"
android:translateY="24"
@@ -46,7 +41,7 @@
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator android:propertyName="pathData"
- android:duration="500"
+ android:duration="250"
android:startOffset="0"
android:valueFrom="M48 -16 C48,-16 48,16 48,16 C48,33.67 33.67,48 16,48 C16,48 -16,48 -16,48 C-33.67,48 -48,33.67 -48,16 C-48,16 -48,-16 -48,-16 C-48,-33.67 -33.67,-48 -16,-48 C-16,-48 16,-48 16,-48 C33.67,-48 48,-33.67 48,-16c "
android:valueTo="M48 0.25 C48,0.25 48,0 48,0 C47.75,26 31.25,48 0,48 C0,48 0,48 0,48 C-30,48 -48,25.75 -48,-0.25 C-48,-0.25 -48,-0.25 -48,-0.25 C-47.75,-23.5 -32.25,-47.75 0.5,-48 C0.5,-48 0.5,-48 0.5,-48 C28,-47.75 47.75,-29.75 48,0.25c "
@@ -62,7 +57,7 @@
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator android:propertyName="translateX"
- android:duration="517"
+ android:duration="267"
android:startOffset="0"
android:valueFrom="0"
android:valueTo="1"
@@ -70,4 +65,4 @@
</set>
</aapt:attr>
</target>
-</animated-vector>
\ No newline at end of file
+</animated-vector>
diff --git a/packages/SystemUI/res/drawable/ic_media_play_container.xml b/packages/SystemUI/res/drawable/ic_media_play_container.xml
index 2fc9fc8..4cb011a 100644
--- a/packages/SystemUI/res/drawable/ic_media_play_container.xml
+++ b/packages/SystemUI/res/drawable/ic_media_play_container.xml
@@ -41,7 +41,7 @@
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator android:propertyName="pathData"
- android:duration="500"
+ android:duration="250"
android:startOffset="0"
android:valueFrom="M48 0.25 C48,0.25 48,0 48,0 C47.75,26 31.25,48 0,48 C0,48 0,48 0,48 C-30,48 -48,25.75 -48,-0.25 C-48,-0.25 -48,-0.25 -48,-0.25 C-47.75,-23.5 -32.25,-47.75 0.5,-48 C0.5,-48 0.5,-48 0.5,-48 C28,-47.75 47.75,-29.75 48,0.25c "
android:valueTo="M48 -16 C48,-16 48,16 48,16 C48,33.67 33.67,48 16,48 C16,48 -16,48 -16,48 C-33.67,48 -48,33.67 -48,16 C-48,16 -48,-16 -48,-16 C-48,-33.67 -33.67,-48 -16,-48 C-16,-48 16,-48 16,-48 C33.67,-48 48,-33.67 48,-16c "
@@ -57,7 +57,7 @@
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator android:propertyName="translateX"
- android:duration="517"
+ android:duration="267"
android:startOffset="0"
android:valueFrom="0"
android:valueTo="1"
@@ -65,4 +65,4 @@
</set>
</aapt:attr>
</target>
-</animated-vector>
\ No newline at end of file
+</animated-vector>
diff --git a/packages/SystemUI/res/layout/keyguard_status_bar.xml b/packages/SystemUI/res/layout/keyguard_status_bar.xml
index e47eed9..d27fa19 100644
--- a/packages/SystemUI/res/layout/keyguard_status_bar.xml
+++ b/packages/SystemUI/res/layout/keyguard_status_bar.xml
@@ -60,9 +60,8 @@
</com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer>
<FrameLayout android:id="@+id/system_icons_container"
- android:layout_width="0dp"
+ android:layout_width="wrap_content"
android:layout_height="match_parent"
- android:layout_weight="1"
android:layout_marginEnd="@dimen/status_bar_padding_end"
android:gravity="center_vertical|end">
<include layout="@layout/system_icons" />
diff --git a/packages/SystemUI/res/layout/notification_icon_area.xml b/packages/SystemUI/res/layout/notification_icon_area.xml
index fa696cc..aadfae8 100644
--- a/packages/SystemUI/res/layout/notification_icon_area.xml
+++ b/packages/SystemUI/res/layout/notification_icon_area.xml
@@ -14,18 +14,9 @@
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
-<com.android.keyguard.AlphaOptimizedLinearLayout
+<com.android.systemui.statusbar.phone.NotificationIconContainer
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/notification_icon_area_inner"
- android:layout_width="match_parent"
+ android:id="@+id/notificationIcons"
+ android:layout_width="wrap_content"
android:layout_height="match_parent"
- android:clipChildren="false">
- <com.android.systemui.statusbar.phone.NotificationIconContainer
- android:id="@+id/notificationIcons"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_alignParentStart="true"
- android:gravity="center_vertical"
- android:orientation="horizontal"
- android:clipChildren="false"/>
-</com.android.keyguard.AlphaOptimizedLinearLayout>
\ No newline at end of file
+ android:clipChildren="false"/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/people_space_activity.xml b/packages/SystemUI/res/layout/people_space_activity.xml
index 7102375..f45cc7c 100644
--- a/packages/SystemUI/res/layout/people_space_activity.xml
+++ b/packages/SystemUI/res/layout/people_space_activity.xml
@@ -13,103 +13,11 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<LinearLayout
+<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
- android:id="@+id/top_level"
+ android:id="@+id/container"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:padding="8dp">
- <TextView
- android:id="@+id/select_conversation_title"
- android:text="@string/select_conversation_title"
- android:gravity="center"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
- android:textColor="?android:attr/textColorPrimary"
- android:textSize="24sp"/>
-
- <TextView
- android:id="@+id/select_conversation"
- android:text="@string/select_conversation_text"
- android:gravity="center"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
- android:textColor="?android:attr/textColorPrimary"
- android:textSize="16sp"
- android:paddingVertical="24dp"
- android:paddingHorizontal="48dp"/>
-
- <androidx.core.widget.NestedScrollView
- android:id="@+id/scroll_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <LinearLayout
- android:id="@+id/scroll_layout"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="16dp"
- android:orientation="vertical">
-
- <LinearLayout
- android:id="@+id/priority"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="35dp">
- <TextView
- android:id="@+id/priority_header"
- android:text="@string/priority_conversations"
- android:layout_width="wrap_content"
- android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
- android:textColor="?androidprv:attr/colorAccentPrimaryVariant"
- android:textSize="14sp"
- android:paddingStart="16dp"
- android:layout_height="wrap_content"/>
-
- <LinearLayout
- android:id="@+id/priority_tiles"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="10dp"
- android:orientation="vertical"
- android:background="@drawable/rounded_bg_full_large_radius"
- android:clipToOutline="true">
- </LinearLayout>
- </LinearLayout>
-
- <LinearLayout
- android:id="@+id/recent"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
- <TextView
- android:id="@+id/recent_header"
- android:gravity="start"
- android:text="@string/recent_conversations"
- android:layout_width="wrap_content"
- android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
- android:textColor="?androidprv:attr/colorAccentPrimaryVariant"
- android:textSize="14sp"
- android:paddingStart="16dp"
- android:layout_height="wrap_content"/>
-
- <LinearLayout
- android:id="@+id/recent_tiles"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="10dp"
- android:orientation="vertical"
- android:background="@drawable/rounded_bg_full_large_radius"
- android:clipToOutline="true">
- </LinearLayout>
- </LinearLayout>
- </LinearLayout>
- </androidx.core.widget.NestedScrollView>
-</LinearLayout>
\ No newline at end of file
+ android:layout_height="match_parent">
+ <!-- The content of people_space_activity_(no|with)_conversations.xml will be added here at
+ runtime depending on the number of conversations to show. -->
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml b/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml
index 2e9ff07..e929169 100644
--- a/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml
+++ b/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml
@@ -16,7 +16,7 @@
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
- android:id="@+id/top_level"
+ android:id="@+id/top_level_no_conversations"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp"
diff --git a/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml b/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml
new file mode 100644
index 0000000..2384963
--- /dev/null
+++ b/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml
@@ -0,0 +1,115 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/top_level_with_conversations"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="8dp">
+ <TextView
+ android:id="@+id/select_conversation_title"
+ android:text="@string/select_conversation_title"
+ android:gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="24sp"/>
+
+ <TextView
+ android:id="@+id/select_conversation"
+ android:text="@string/select_conversation_text"
+ android:gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="16sp"
+ android:paddingVertical="24dp"
+ android:paddingHorizontal="48dp"/>
+
+ <androidx.core.widget.NestedScrollView
+ android:id="@+id/scroll_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:id="@+id/scroll_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/priority"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="35dp">
+ <TextView
+ android:id="@+id/priority_header"
+ android:text="@string/priority_conversations"
+ android:layout_width="wrap_content"
+ android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
+ android:textColor="?androidprv:attr/colorAccentPrimaryVariant"
+ android:textSize="14sp"
+ android:paddingStart="16dp"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:id="@+id/priority_tiles"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:orientation="vertical"
+ android:background="@drawable/rounded_bg_full_large_radius"
+ android:clipToOutline="true">
+ </LinearLayout>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/recent"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/recent_header"
+ android:gravity="start"
+ android:text="@string/recent_conversations"
+ android:layout_width="wrap_content"
+ android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
+ android:textColor="?androidprv:attr/colorAccentPrimaryVariant"
+ android:textSize="14sp"
+ android:paddingStart="16dp"
+ android:layout_height="wrap_content"/>
+
+ <LinearLayout
+ android:id="@+id/recent_tiles"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:orientation="vertical"
+ android:background="@drawable/rounded_bg_full_large_radius"
+ android:clipToOutline="true">
+ </LinearLayout>
+ </LinearLayout>
+ </LinearLayout>
+ </androidx.core.widget.NestedScrollView>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/people_space_tile_view.xml b/packages/SystemUI/res/layout/people_space_tile_view.xml
index 2a2c35d..b0599ca 100644
--- a/packages/SystemUI/res/layout/people_space_tile_view.xml
+++ b/packages/SystemUI/res/layout/people_space_tile_view.xml
@@ -37,8 +37,8 @@
<ImageView
android:id="@+id/tile_view_person_icon"
- android:layout_width="52dp"
- android:layout_height="52dp" />
+ android:layout_width="@dimen/avatar_size_for_medium"
+ android:layout_height="@dimen/avatar_size_for_medium" />
<LinearLayout
android:orientation="horizontal"
diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml
index deab1eb..e281511 100644
--- a/packages/SystemUI/res/layout/status_bar.xml
+++ b/packages/SystemUI/res/layout/status_bar.xml
@@ -47,52 +47,63 @@
android:paddingStart="@dimen/status_bar_padding_start"
android:paddingEnd="@dimen/status_bar_padding_end"
android:paddingTop="@dimen/status_bar_padding_top"
- android:orientation="horizontal"
- >
+ android:orientation="horizontal">
+
+ <!-- Container for the entire start half of the status bar. It will always use the same
+ width, independent of the number of visible children and sub-children. -->
<FrameLayout
+ android:id="@+id/status_bar_start_side_container"
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_weight="1">
- <include layout="@layout/heads_up_status_bar_layout" />
+ <!-- Container that is wrapped around the views on the start half of the status bar.
+ Its width will change with the number of visible children and sub-children.
+ It is useful when we want to know the visible bounds of the content. -->
+ <FrameLayout
+ android:id="@+id/status_bar_start_side_content"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipChildren="false">
- <!-- The alpha of the left side is controlled by PhoneStatusBarTransitions, and the
- individual views are controlled by StatusBarManager disable flags DISABLE_CLOCK and
- DISABLE_NOTIFICATION_ICONS, respectively -->
- <LinearLayout
- android:id="@+id/status_bar_left_side"
- android:layout_height="match_parent"
- android:layout_width="match_parent"
- android:clipChildren="false"
- >
- <ViewStub
- android:id="@+id/operator_name"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout="@layout/operator_name" />
+ <include layout="@layout/heads_up_status_bar_layout" />
- <com.android.systemui.statusbar.policy.Clock
- android:id="@+id/clock"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:textAppearance="@style/TextAppearance.StatusBar.Clock"
- android:singleLine="true"
- android:paddingStart="@dimen/status_bar_left_clock_starting_padding"
- android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
- android:gravity="center_vertical|start"
- />
+ <!-- The alpha of the start side is controlled by PhoneStatusBarTransitions, and the
+ individual views are controlled by StatusBarManager disable flags DISABLE_CLOCK
+ and DISABLE_NOTIFICATION_ICONS, respectively -->
+ <LinearLayout
+ android:id="@+id/status_bar_start_side_except_heads_up"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:clipChildren="false">
+ <ViewStub
+ android:id="@+id/operator_name"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout="@layout/operator_name" />
- <include layout="@layout/ongoing_call_chip" />
+ <com.android.systemui.statusbar.policy.Clock
+ android:id="@+id/clock"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:textAppearance="@style/TextAppearance.StatusBar.Clock"
+ android:singleLine="true"
+ android:paddingStart="@dimen/status_bar_left_clock_starting_padding"
+ android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
+ android:gravity="center_vertical|start"
+ />
- <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
- android:id="@+id/notification_icon_area"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:orientation="horizontal"
- android:clipChildren="false"/>
+ <include layout="@layout/ongoing_call_chip" />
- </LinearLayout>
+ <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
+ android:id="@+id/notification_icon_area"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:clipChildren="false"/>
+
+ </LinearLayout>
+ </FrameLayout>
</FrameLayout>
<!-- Space should cover the notch (if it exists) and let other views lay out around it -->
@@ -103,42 +114,57 @@
android:gravity="center_horizontal|center_vertical"
/>
- <com.android.keyguard.AlphaOptimizedLinearLayout android:id="@+id/system_icon_area"
+ <!-- Container for the entire end half of the status bar. It will always use the same
+ width, independent of the number of visible children and sub-children. -->
+ <FrameLayout
+ android:id="@+id/status_bar_end_side_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
- android:orientation="horizontal"
- android:gravity="center_vertical|end"
- >
+ android:clipChildren="false">
- <com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer
- android:id="@+id/user_switcher_container"
+ <!-- Container that is wrapped around the views on the end half of the
+ status bar. Its width will change with the number of visible children and
+ sub-children.
+ It is useful when we want know the visible bounds of the content.-->
+ <com.android.keyguard.AlphaOptimizedLinearLayout
+ android:id="@+id/status_bar_end_side_content"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="center"
+ android:layout_height="match_parent"
+ android:layout_gravity="end"
android:orientation="horizontal"
- android:paddingTop="4dp"
- android:paddingBottom="4dp"
- android:paddingStart="8dp"
- android:paddingEnd="8dp"
- android:layout_marginEnd="16dp"
- android:background="@drawable/status_bar_user_chip_bg"
- android:visibility="visible" >
- <ImageView android:id="@+id/current_user_avatar"
- android:layout_width="@dimen/multi_user_avatar_keyguard_size"
- android:layout_height="@dimen/multi_user_avatar_keyguard_size"
- android:scaleType="centerInside"
- android:paddingEnd="4dp" />
+ android:gravity="center_vertical|end"
+ android:clipChildren="false">
- <TextView android:id="@+id/current_user_name"
+ <com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer
+ android:id="@+id/user_switcher_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textAppearance="@style/TextAppearance.StatusBar.Clock"
- />
- </com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer>
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ android:layout_marginEnd="16dp"
+ android:background="@drawable/status_bar_user_chip_bg"
+ android:visibility="visible" >
+ <ImageView android:id="@+id/current_user_avatar"
+ android:layout_width="@dimen/multi_user_avatar_keyguard_size"
+ android:layout_height="@dimen/multi_user_avatar_keyguard_size"
+ android:scaleType="centerInside"
+ android:paddingEnd="4dp" />
- <include layout="@layout/system_icons" />
- </com.android.keyguard.AlphaOptimizedLinearLayout>
+ <TextView android:id="@+id/current_user_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.StatusBar.Clock"
+ />
+ </com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer>
+
+ <include layout="@layout/system_icons" />
+ </com.android.keyguard.AlphaOptimizedLinearLayout>
+ </FrameLayout>
</LinearLayout>
<ViewStub
diff --git a/packages/SystemUI/screenshot/Android.bp b/packages/SystemUI/screenshot/Android.bp
index 601e92f..f449398 100644
--- a/packages/SystemUI/screenshot/Android.bp
+++ b/packages/SystemUI/screenshot/Android.bp
@@ -38,6 +38,7 @@
"androidx.test.espresso.core",
"androidx.appcompat_appcompat",
"platform-screenshot-diff-core",
+ "guava",
],
kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/screenshot/res/values/themes.xml b/packages/SystemUI/screenshot/res/values/themes.xml
index 40e50bb..a7f8a26 100644
--- a/packages/SystemUI/screenshot/res/values/themes.xml
+++ b/packages/SystemUI/screenshot/res/values/themes.xml
@@ -19,6 +19,12 @@
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
+ <!-- We make the status and navigation bars transparent so that the screenshotted content is
+ not clipped by the status bar height when drawn into the Bitmap (which is what happens
+ given that we draw the view into the Bitmap using hardware acceleration). -->
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+
<!-- Make sure that device specific cutouts don't impact the outcome of screenshot tests -->
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/Bitmap.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/Bitmap.kt
index 3d26cda..a4a70a4 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/Bitmap.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/Bitmap.kt
@@ -24,6 +24,8 @@
import platform.test.screenshot.matchers.PixelPerfectMatcher
/** Draw this [View] into a [Bitmap]. */
+// TODO(b/195673633): Remove this once Compose screenshot tests use hardware rendering for their
+// tests.
fun View.drawIntoBitmap(): Bitmap {
val bitmap =
Bitmap.createBitmap(
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewCapture.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewCapture.kt
new file mode 100644
index 0000000..c609e6f
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewCapture.kt
@@ -0,0 +1,180 @@
+package com.android.systemui.testing.screenshot
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.PixelCopy
+import android.view.SurfaceView
+import android.view.View
+import android.view.ViewTreeObserver
+import android.view.Window
+import androidx.annotation.RequiresApi
+import androidx.concurrent.futures.ResolvableFuture
+import androidx.test.annotation.ExperimentalTestApi
+import androidx.test.core.internal.os.HandlerExecutor
+import androidx.test.platform.graphics.HardwareRendererCompat
+import com.google.common.util.concurrent.ListenableFuture
+
+/*
+ * This file was forked from androidx/test/core/view/ViewCapture.kt to add [Window] parameter to
+ * [View.captureToBitmap].
+ * TODO(b/195673633): Remove this fork and use the AndroidX version instead.
+ */
+
+/**
+ * Asynchronously captures an image of the underlying view into a [Bitmap].
+ *
+ * For devices below [Build.VERSION_CODES#O] (or if the view's window cannot be determined), the
+ * image is obtained using [View#draw]. Otherwise, [PixelCopy] is used.
+ *
+ * This method will also enable [HardwareRendererCompat#setDrawingEnabled(boolean)] if required.
+ *
+ * This API is primarily intended for use in lower layer libraries or frameworks. For test authors,
+ * its recommended to use espresso or compose's captureToImage.
+ *
+ * This API is currently experimental and subject to change or removal.
+ */
+@ExperimentalTestApi
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+fun View.captureToBitmap(window: Window? = null): ListenableFuture<Bitmap> {
+ val bitmapFuture: ResolvableFuture<Bitmap> = ResolvableFuture.create()
+ val mainExecutor = HandlerExecutor(Handler(Looper.getMainLooper()))
+
+ // disable drawing again if necessary once work is complete
+ if (!HardwareRendererCompat.isDrawingEnabled()) {
+ HardwareRendererCompat.setDrawingEnabled(true)
+ bitmapFuture.addListener({ HardwareRendererCompat.setDrawingEnabled(false) }, mainExecutor)
+ }
+
+ mainExecutor.execute {
+ val forceRedrawFuture = forceRedraw()
+ forceRedrawFuture.addListener({ generateBitmap(bitmapFuture, window) }, mainExecutor)
+ }
+
+ return bitmapFuture
+}
+
+/**
+ * Trigger a redraw of the given view.
+ *
+ * Should only be called on UI thread.
+ *
+ * @return a [ListenableFuture] that will be complete once ui drawing is complete
+ */
+// NoClassDefFoundError occurs on API 15
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+// @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@ExperimentalTestApi
+fun View.forceRedraw(): ListenableFuture<Void> {
+ val future: ResolvableFuture<Void> = ResolvableFuture.create()
+
+ if (Build.VERSION.SDK_INT >= 29 && isHardwareAccelerated) {
+ viewTreeObserver.registerFrameCommitCallback() { future.set(null) }
+ } else {
+ viewTreeObserver.addOnDrawListener(
+ object : ViewTreeObserver.OnDrawListener {
+ var handled = false
+ override fun onDraw() {
+ if (!handled) {
+ handled = true
+ future.set(null)
+ // cannot remove on draw listener inside of onDraw
+ Handler(Looper.getMainLooper()).post {
+ viewTreeObserver.removeOnDrawListener(this)
+ }
+ }
+ }
+ }
+ )
+ }
+ invalidate()
+ return future
+}
+
+private fun View.generateBitmap(
+ bitmapFuture: ResolvableFuture<Bitmap>,
+ window: Window? = null,
+) {
+ if (bitmapFuture.isCancelled) {
+ return
+ }
+ val destBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ when {
+ Build.VERSION.SDK_INT < 26 -> generateBitmapFromDraw(destBitmap, bitmapFuture)
+ this is SurfaceView -> generateBitmapFromSurfaceViewPixelCopy(destBitmap, bitmapFuture)
+ else -> {
+ val window = window ?: getActivity()?.window
+ if (window != null) {
+ generateBitmapFromPixelCopy(window, destBitmap, bitmapFuture)
+ } else {
+ Log.i(
+ "View.captureToImage",
+ "Could not find window for view. Falling back to View#draw instead of PixelCopy"
+ )
+ generateBitmapFromDraw(destBitmap, bitmapFuture)
+ }
+ }
+ }
+}
+
+@SuppressWarnings("NewApi")
+private fun SurfaceView.generateBitmapFromSurfaceViewPixelCopy(
+ destBitmap: Bitmap,
+ bitmapFuture: ResolvableFuture<Bitmap>
+) {
+ val onCopyFinished =
+ PixelCopy.OnPixelCopyFinishedListener { result ->
+ if (result == PixelCopy.SUCCESS) {
+ bitmapFuture.set(destBitmap)
+ } else {
+ bitmapFuture.setException(
+ RuntimeException(String.format("PixelCopy failed: %d", result))
+ )
+ }
+ }
+ PixelCopy.request(this, null, destBitmap, onCopyFinished, handler)
+}
+
+internal fun View.generateBitmapFromDraw(
+ destBitmap: Bitmap,
+ bitmapFuture: ResolvableFuture<Bitmap>
+) {
+ destBitmap.density = resources.displayMetrics.densityDpi
+ computeScroll()
+ val canvas = Canvas(destBitmap)
+ canvas.translate((-scrollX).toFloat(), (-scrollY).toFloat())
+ draw(canvas)
+ bitmapFuture.set(destBitmap)
+}
+
+private fun View.getActivity(): Activity? {
+ fun Context.getActivity(): Activity? {
+ return when (this) {
+ is Activity -> this
+ is ContextWrapper -> this.baseContext.getActivity()
+ else -> null
+ }
+ }
+ return context.getActivity()
+}
+
+private fun View.generateBitmapFromPixelCopy(
+ window: Window,
+ destBitmap: Bitmap,
+ bitmapFuture: ResolvableFuture<Bitmap>
+) {
+ val locationInWindow = intArrayOf(0, 0)
+ getLocationInWindow(locationInWindow)
+ val x = locationInWindow[0]
+ val y = locationInWindow[1]
+ val boundsInWindow = Rect(x, y, x + width, y + height)
+
+ return window.generateBitmapFromPixelCopy(boundsInWindow, destBitmap, bitmapFuture)
+}
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
index 3209c8b..60130e1 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
@@ -18,10 +18,22 @@
import android.app.Activity
import android.app.Dialog
+import android.graphics.Bitmap
+import android.graphics.HardwareRenderer
+import android.os.Looper
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.Window
+import androidx.activity.ComponentActivity
+import androidx.test.espresso.Espresso
import androidx.test.ext.junit.rules.ActivityScenarioRule
+import com.google.common.util.concurrent.FutureCallback
+import com.google.common.util.concurrent.Futures
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
@@ -59,29 +71,39 @@
*/
fun screenshotTest(
goldenIdentifier: String,
- layoutParams: LayoutParams =
- LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT),
- viewProvider: (Activity) -> View,
+ mode: Mode = Mode.WrapContent,
+ viewProvider: (ComponentActivity) -> View,
) {
activityRule.scenario.onActivity { activity ->
// Make sure that the activity draws full screen and fits the whole display instead of
// the system bars.
- activity.window.setDecorFitsSystemWindows(false)
- activity.setContentView(viewProvider(activity), layoutParams)
+ val window = activity.window
+ window.setDecorFitsSystemWindows(false)
+
+ // Set the content.
+ activity.setContentView(viewProvider(activity), mode.layoutParams)
+
+ // Elevation/shadows is not deterministic when doing hardware rendering, so we disable
+ // it for any view in the hierarchy.
+ window.decorView.removeElevationRecursively()
}
// We call onActivity again because it will make sure that our Activity is done measuring,
// laying out and drawing its content (that we set in the previous onActivity lambda).
+ var contentView: View? = null
activityRule.scenario.onActivity { activity ->
// Check that the content is what we expected.
val content = activity.requireViewById<ViewGroup>(android.R.id.content)
assertEquals(1, content.childCount)
- screenshotRule.assertBitmapAgainstGolden(
- content.getChildAt(0).drawIntoBitmap(),
- goldenIdentifier,
- matcher
- )
+ contentView = content.getChildAt(0)
}
+
+ val bitmap = contentView?.toBitmap() ?: error("contentView is null")
+ screenshotRule.assertBitmapAgainstGolden(
+ bitmap,
+ goldenIdentifier,
+ matcher,
+ )
}
/**
@@ -104,25 +126,78 @@
create()
window.setWindowAnimations(0)
+ // Elevation/shadows is not deterministic when doing hardware rendering, so we
+ // disable it for any view in the hierarchy.
+ window.decorView.removeElevationRecursively()
+
// Show the dialog.
show()
}
}
- // We call onActivity again because it will make sure that our Dialog is done measuring,
- // laying out and drawing its content (that we set in the previous onActivity lambda).
- activityRule.scenario.onActivity {
- // Check that the content is what we expected.
- val dialog = dialog ?: error("dialog is null")
- try {
- screenshotRule.assertBitmapAgainstGolden(
- dialog.window.decorView.drawIntoBitmap(),
- goldenIdentifier,
- matcher,
+ try {
+ val bitmap = dialog?.toBitmap() ?: error("dialog is null")
+ screenshotRule.assertBitmapAgainstGolden(
+ bitmap,
+ goldenIdentifier,
+ matcher,
+ )
+ } finally {
+ dialog?.dismiss()
+ }
+ }
+
+ private fun View.removeElevationRecursively() {
+ this.elevation = 0f
+
+ if (this is ViewGroup) {
+ repeat(childCount) { i -> getChildAt(i).removeElevationRecursively() }
+ }
+ }
+
+ private fun Dialog.toBitmap(): Bitmap {
+ val window = window
+ return window.decorView.toBitmap(window)
+ }
+
+ private fun View.toBitmap(window: Window? = null): Bitmap {
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ error("toBitmap() can't be called from the main thread")
+ }
+
+ if (!HardwareRenderer.isDrawingEnabled()) {
+ error("Hardware rendering is not enabled")
+ }
+
+ // Make sure we are idle.
+ Espresso.onIdle()
+
+ val mainExecutor = context.mainExecutor
+ return runBlocking {
+ suspendCoroutine { continuation ->
+ Futures.addCallback(
+ captureToBitmap(window),
+ object : FutureCallback<Bitmap> {
+ override fun onSuccess(result: Bitmap?) {
+ continuation.resumeWith(Result.success(result!!))
+ }
+
+ override fun onFailure(t: Throwable) {
+ continuation.resumeWith(Result.failure(t))
+ }
+ },
+ // We know that we are not on the main thread, so we can block the current
+ // thread and wait for the result in the main thread.
+ mainExecutor,
)
- } finally {
- dialog.dismiss()
}
}
}
+
+ enum class Mode(val layoutParams: LayoutParams) {
+ WrapContent(LayoutParams(WRAP_CONTENT, WRAP_CONTENT)),
+ MatchSize(LayoutParams(MATCH_PARENT, MATCH_PARENT)),
+ MatchWidth(LayoutParams(MATCH_PARENT, WRAP_CONTENT)),
+ MatchHeight(LayoutParams(WRAP_CONTENT, MATCH_PARENT)),
+ }
}
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/WindowCapture.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/WindowCapture.kt
new file mode 100644
index 0000000..d34f46b
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/WindowCapture.kt
@@ -0,0 +1,37 @@
+package com.android.systemui.testing.screenshot
+
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.os.Handler
+import android.os.Looper
+import android.view.PixelCopy
+import android.view.Window
+import androidx.concurrent.futures.ResolvableFuture
+
+/*
+ * This file was forked from androidx/test/core/view/WindowCapture.kt.
+ * TODO(b/195673633): Remove this fork and use the AndroidX version instead.
+ */
+fun Window.generateBitmapFromPixelCopy(
+ boundsInWindow: Rect? = null,
+ destBitmap: Bitmap,
+ bitmapFuture: ResolvableFuture<Bitmap>
+) {
+ val onCopyFinished =
+ PixelCopy.OnPixelCopyFinishedListener { result ->
+ if (result == PixelCopy.SUCCESS) {
+ bitmapFuture.set(destBitmap)
+ } else {
+ bitmapFuture.setException(
+ RuntimeException(String.format("PixelCopy failed: %d", result))
+ )
+ }
+ }
+ PixelCopy.request(
+ this,
+ boundsInWindow,
+ destBitmap,
+ onCopyFinished,
+ Handler(Looper.getMainLooper())
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/Somnambulator.java b/packages/SystemUI/src/com/android/systemui/Somnambulator.java
index 0dd6d92..25801cf 100644
--- a/packages/SystemUI/src/com/android/systemui/Somnambulator.java
+++ b/packages/SystemUI/src/com/android/systemui/Somnambulator.java
@@ -17,12 +17,15 @@
package com.android.systemui;
import android.app.Activity;
-import android.content.Intent;
import android.service.dreams.Sandman;
/**
* A simple activity that launches a dream.
* <p>
+ *
+ * This activity has been deprecated and no longer used. The system uses its presence to determine
+ * whether a dock app should be started on dock through intent resolution.
+ *
* Note: This Activity is special. If this class is moved to another package or
* renamed, be sure to update the component name in {@link Sandman}.
* </p>
@@ -34,27 +37,6 @@
@Override
public void onStart() {
super.onStart();
-
- final Intent launchIntent = getIntent();
- final String action = launchIntent.getAction();
- if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
- Intent shortcutIntent = new Intent(this, Somnambulator.class);
- shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
- | Intent.FLAG_ACTIVITY_NEW_TASK);
- Intent resultIntent = new Intent();
- resultIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
- Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher_dreams));
- resultIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
- resultIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, getString(R.string.start_dreams));
- setResult(RESULT_OK, resultIntent);
- } else {
- boolean docked = launchIntent.hasCategory(Intent.CATEGORY_DESK_DOCK);
- if (docked) {
- Sandman.startDreamWhenDockedIfAppropriate(this);
- } else {
- Sandman.startDreamByUserRequest(this);
- }
- }
finish();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
index d2703f5..aff0b1f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
@@ -353,6 +353,7 @@
}
mIsShowing = false;
+ mDragAnimator.cancel();
mWindowManager.removeView(this);
setOnApplyWindowInsetsListener(null);
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 0e0073a..fe96222 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -45,6 +45,7 @@
import com.android.systemui.media.dagger.MediaProjectionModule;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationBarComponent;
+import com.android.systemui.people.PeopleModule;
import com.android.systemui.plugins.BcSmartspaceDataPlugin;
import com.android.systemui.privacy.PrivacyModule;
import com.android.systemui.recents.Recents;
@@ -124,6 +125,7 @@
LogModule.class,
MediaProjectionModule.class,
PeopleHubModule.class,
+ PeopleModule.class,
PluginModule.class,
PrivacyModule.class,
QsFrameTranslateModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/AirQualityColorPicker.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/AirQualityColorPicker.java
deleted file mode 100644
index 328753f..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/AirQualityColorPicker.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.dreams.complication;
-
-import static com.android.systemui.dreams.complication.dagger.DreamAirQualityComplicationComponent.DreamAirQualityComplicationModule.DREAM_AQI_COLOR_DEFAULT;
-import static com.android.systemui.dreams.complication.dagger.DreamAirQualityComplicationComponent.DreamAirQualityComplicationModule.DREAM_AQI_COLOR_THRESHOLDS;
-import static com.android.systemui.dreams.complication.dagger.DreamAirQualityComplicationComponent.DreamAirQualityComplicationModule.DREAM_AQI_COLOR_VALUES;
-
-import android.util.Log;
-
-import androidx.annotation.ColorInt;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-final class AirQualityColorPicker {
- private static final String TAG = "AirQualityColorPicker";
- private final int[] mThresholds;
- private final int[] mColorValues;
- private final int mDefaultColor;
-
- @Inject
- AirQualityColorPicker(@Named(DREAM_AQI_COLOR_THRESHOLDS) int[] thresholds,
- @Named(DREAM_AQI_COLOR_VALUES) int[] colorValues,
- @Named(DREAM_AQI_COLOR_DEFAULT) @ColorInt int defaultColor) {
- mThresholds = thresholds;
- mColorValues = colorValues;
- mDefaultColor = defaultColor;
- }
-
- @ColorInt
- int getColorForValue(String aqiString) {
- int size = mThresholds.length;
- if (mThresholds.length != mColorValues.length) {
- size = Math.min(mThresholds.length, mColorValues.length);
- Log.e(TAG,
- "Threshold size ("
- + mThresholds.length + ") does not match color value size ("
- + mColorValues.length
- + "). Taking the minimum, some values may be ignored.");
-
- }
- try {
- final int value = Integer.parseInt(aqiString.replaceAll("[^0-9]", ""));
- for (int i = size - 1; i >= 0; i--) {
- if (value > mThresholds[i]) {
- return mColorValues[i];
- }
- }
- Log.e(TAG, "No matching AQI color for value: " + value);
- } catch (NumberFormatException e) {
- Log.e(TAG, "Could not read AQI value from:" + aqiString);
- }
- return mDefaultColor;
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamAirQualityComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamAirQualityComplication.java
deleted file mode 100644
index ba63303..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamAirQualityComplication.java
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.dreams.complication;
-
-import static com.android.systemui.dreams.complication.dagger.DreamAirQualityComplicationComponent.DreamAirQualityComplicationModule.DREAM_AQI_COMPLICATION_LAYOUT_PARAMS;
-import static com.android.systemui.dreams.complication.dagger.DreamAirQualityComplicationComponent.DreamAirQualityComplicationModule.DREAM_AQI_COMPLICATION_VIEW;
-import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.SMARTSPACE_TRAMPOLINE_ACTIVITY_COMPONENT;
-
-import android.app.smartspace.SmartspaceAction;
-import android.app.smartspace.SmartspaceTarget;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.ShapeDrawable;
-import android.text.TextUtils;
-import android.view.View;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.android.systemui.CoreStartable;
-import com.android.systemui.dreams.DreamOverlayStateController;
-import com.android.systemui.dreams.complication.dagger.DreamAirQualityComplicationComponent;
-import com.android.systemui.dreams.smartspace.DreamSmartspaceController;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener;
-import com.android.systemui.util.ViewController;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-/**
- * Air quality complication which produces view holder responsible for showing AQI over dreams.
- */
-public class DreamAirQualityComplication implements Complication {
- // TODO(b/236024839): Move to SmartspaceTarget
- public static final int FEATURE_AIR_QUALITY = 46;
-
- private final DreamAirQualityComplicationComponent.Factory mComponentFactory;
-
- @Inject
- public DreamAirQualityComplication(
- DreamAirQualityComplicationComponent.Factory componentFactory) {
- mComponentFactory = componentFactory;
- }
-
- @Override
- public int getRequiredTypeAvailability() {
- return COMPLICATION_TYPE_AIR_QUALITY;
- }
-
- @Override
- public ViewHolder createView(ComplicationViewModel model) {
- return mComponentFactory.create().getViewHolder();
- }
-
- /**
- * {@link CoreStartable} for registering {@link DreamAirQualityComplication} with SystemUI.
- */
- public static class Registrant extends CoreStartable {
- private final DreamOverlayStateController mDreamOverlayStateController;
- private final DreamAirQualityComplication mComplication;
-
- /**
- * Default constructor to register {@link DreamAirQualityComplication}.
- */
- @Inject
- public Registrant(Context context,
- DreamOverlayStateController dreamOverlayStateController,
- DreamAirQualityComplication complication) {
- super(context);
- mDreamOverlayStateController = dreamOverlayStateController;
- mComplication = complication;
- }
-
- @Override
- public void start() {
- // TODO(b/221500478): Only add complication once we have data to show.
- mDreamOverlayStateController.addComplication(mComplication);
- }
- }
-
- /**
- * ViewHolder to contain value/logic associated with the AQI complication view.
- */
- public static class DreamAirQualityViewHolder implements ViewHolder {
- private final TextView mView;
- private final DreamAirQualityViewController mController;
- private final ComplicationLayoutParams mLayoutParams;
-
- @Inject
- DreamAirQualityViewHolder(@Named(DREAM_AQI_COMPLICATION_VIEW) TextView view,
- DreamAirQualityViewController controller,
- @Named(DREAM_AQI_COMPLICATION_LAYOUT_PARAMS)
- ComplicationLayoutParams layoutParams) {
- mView = view;
- mLayoutParams = layoutParams;
- mController = controller;
- mController.init();
- }
-
- @Override
- public View getView() {
- return mView;
- }
-
- @Override
- public ComplicationLayoutParams getLayoutParams() {
- return mLayoutParams;
- }
- }
-
- static class DreamAirQualityViewController extends ViewController<TextView> {
- private final DreamSmartspaceController mSmartspaceController;
- private final String mSmartspaceTrampolineComponent;
- private final ActivityStarter mActivityStarter;
- private final AirQualityColorPicker mAirQualityColorPicker;
-
- private final SmartspaceTargetListener mSmartspaceTargetListener = targets -> {
- final SmartspaceTarget target = targets.stream()
- .filter(t -> t instanceof SmartspaceTarget)
- .map(t -> (SmartspaceTarget) t)
- .filter(t -> t.getFeatureType() == FEATURE_AIR_QUALITY)
- .findFirst()
- .orElse(null);
- updateView(target);
- };
-
- @Inject
- DreamAirQualityViewController(@Named(DREAM_AQI_COMPLICATION_VIEW) TextView view,
- DreamSmartspaceController smartspaceController,
- @Named(SMARTSPACE_TRAMPOLINE_ACTIVITY_COMPONENT)
- String smartspaceTrampolineComponent,
- ActivityStarter activityStarter,
- AirQualityColorPicker airQualityColorPicker) {
- super(view);
- mSmartspaceController = smartspaceController;
- mSmartspaceTrampolineComponent = smartspaceTrampolineComponent;
- mActivityStarter = activityStarter;
- mAirQualityColorPicker = airQualityColorPicker;
- }
-
- @Override
- protected void onViewAttached() {
- mSmartspaceController.addUnfilteredListener(mSmartspaceTargetListener);
- }
-
- @Override
- protected void onViewDetached() {
- mSmartspaceController.removeUnfilteredListener(mSmartspaceTargetListener);
- }
-
- private void updateView(@Nullable SmartspaceTarget target) {
- final SmartspaceAction headerAction = target == null ? null : target.getHeaderAction();
- if (headerAction == null || TextUtils.isEmpty(headerAction.getTitle())) {
- mView.setVisibility(View.GONE);
- return;
- }
- mView.setVisibility(View.VISIBLE);
-
- final String airQuality = headerAction.getTitle().toString();
- mView.setText(airQuality);
-
- final Drawable background = mView.getBackground().mutate();
- final int color = mAirQualityColorPicker.getColorForValue(airQuality);
-
- if (background instanceof ShapeDrawable) {
- ((ShapeDrawable) background).getPaint().setColor(color);
- } else if (background instanceof GradientDrawable) {
- ((GradientDrawable) background).setColor(color);
- }
- mView.setBackground(background);
-
- final Intent intent = headerAction.getIntent();
- if (intent != null && intent.getComponent() != null
- && intent.getComponent().getClassName().equals(
- mSmartspaceTrampolineComponent)) {
- mView.setOnClickListener(v -> {
- mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0);
- });
- }
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamWeatherComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamWeatherComplication.java
deleted file mode 100644
index ce61b16..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamWeatherComplication.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.dreams.complication;
-
-import static com.android.systemui.dreams.complication.dagger.DreamWeatherComplicationComponent.DreamWeatherComplicationModule.DREAM_WEATHER_COMPLICATION_LAYOUT_PARAMS;
-import static com.android.systemui.dreams.complication.dagger.DreamWeatherComplicationComponent.DreamWeatherComplicationModule.DREAM_WEATHER_COMPLICATION_VIEW;
-import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.SMARTSPACE_TRAMPOLINE_ACTIVITY_COMPONENT;
-
-import android.app.smartspace.SmartspaceAction;
-import android.app.smartspace.SmartspaceTarget;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
-import android.text.TextUtils;
-import android.widget.TextView;
-
-import com.android.systemui.CoreStartable;
-import com.android.systemui.R;
-import com.android.systemui.dreams.DreamOverlayStateController;
-import com.android.systemui.dreams.complication.dagger.DreamWeatherComplicationComponent;
-import com.android.systemui.dreams.smartspace.DreamSmartspaceController;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener;
-import com.android.systemui.util.ViewController;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-/**
- * Weather Complication that produce Weather view holder.
- */
-public class DreamWeatherComplication implements Complication {
- DreamWeatherComplicationComponent.Factory mComponentFactory;
-
- /**
- * Default constructor for {@link DreamWeatherComplication}.
- */
- @Inject
- public DreamWeatherComplication(
- DreamWeatherComplicationComponent.Factory componentFactory) {
- mComponentFactory = componentFactory;
- }
-
- @Override
- public int getRequiredTypeAvailability() {
- return COMPLICATION_TYPE_WEATHER;
- }
-
- /**
- * Create {@link DreamWeatherViewHolder}.
- */
- @Override
- public ViewHolder createView(ComplicationViewModel model) {
- return mComponentFactory.create().getViewHolder();
- }
-
- /**
- * {@link CoreStartable} for registering {@link DreamWeatherComplication} with SystemUI.
- */
- public static class Registrant extends CoreStartable {
- private final DreamOverlayStateController mDreamOverlayStateController;
- private final DreamWeatherComplication mComplication;
-
- /**
- * Default constructor to register {@link DreamWeatherComplication}.
- */
- @Inject
- public Registrant(Context context,
- DreamOverlayStateController dreamOverlayStateController,
- DreamWeatherComplication dreamWeatherComplication) {
- super(context);
- mDreamOverlayStateController = dreamOverlayStateController;
- mComplication = dreamWeatherComplication;
- }
-
- @Override
- public void start() {
- mDreamOverlayStateController.addComplication(mComplication);
- }
- }
-
- /**
- * ViewHolder to contain value/logic associated with a Weather Complication View.
- */
- public static class DreamWeatherViewHolder implements ViewHolder {
- private final TextView mView;
- private final ComplicationLayoutParams mLayoutParams;
- private final DreamWeatherViewController mViewController;
-
- @Inject
- DreamWeatherViewHolder(
- @Named(DREAM_WEATHER_COMPLICATION_VIEW) TextView view,
- DreamWeatherViewController controller,
- @Named(DREAM_WEATHER_COMPLICATION_LAYOUT_PARAMS)
- ComplicationLayoutParams layoutParams) {
- mView = view;
- mLayoutParams = layoutParams;
- mViewController = controller;
- mViewController.init();
- }
-
- @Override
- public TextView getView() {
- return mView;
- }
-
- @Override
- public ComplicationLayoutParams getLayoutParams() {
- return mLayoutParams;
- }
- }
-
- /**
- * ViewController to contain value/logic associated with a Weather Complication View.
- */
- static class DreamWeatherViewController extends ViewController<TextView> {
- private final DreamSmartspaceController mSmartSpaceController;
- private final ActivityStarter mActivityStarter;
- private final String mSmartspaceTrampolineActivityComponent;
- private SmartspaceTargetListener mSmartspaceTargetListener;
- private final Resources mResources;
-
- @Inject
- DreamWeatherViewController(
- @Named(DREAM_WEATHER_COMPLICATION_VIEW) TextView view,
- @Named(SMARTSPACE_TRAMPOLINE_ACTIVITY_COMPONENT) String smartspaceTrampoline,
- ActivityStarter activityStarter,
- DreamSmartspaceController smartspaceController,
- Resources resources
- ) {
- super(view);
- mActivityStarter = activityStarter;
- mResources = resources;
- mSmartSpaceController = smartspaceController;
- mSmartspaceTrampolineActivityComponent = smartspaceTrampoline;
- }
-
- @Override
- protected void onViewAttached() {
- mSmartspaceTargetListener = targets -> targets.forEach(
- t -> {
- if (t instanceof SmartspaceTarget
- && ((SmartspaceTarget) t).getFeatureType()
- == SmartspaceTarget.FEATURE_WEATHER) {
- final SmartspaceTarget target = (SmartspaceTarget) t;
- final SmartspaceAction headerAction = target.getHeaderAction();
- if (headerAction == null || TextUtils.isEmpty(
- headerAction.getTitle())) {
- return;
- }
-
- final CharSequence temperature = headerAction.getTitle();
- mView.setText(temperature);
- mView.setContentDescription(getFormattedContentDescription(temperature,
- headerAction.getContentDescription()));
- final Icon icon = headerAction.getIcon();
- if (icon != null) {
- final int iconSize =
- getResources().getDimensionPixelSize(
- R.dimen.smart_action_button_icon_size);
- final Drawable iconDrawable = icon.loadDrawable(getContext());
- iconDrawable.setBounds(0, 0, iconSize, iconSize);
- mView.setCompoundDrawables(iconDrawable, null, null, null);
- mView.setCompoundDrawablePadding(
- getResources().getDimensionPixelSize(
- R.dimen.smart_action_button_icon_padding));
- }
- mView.setOnClickListener(v -> {
- final Intent intent = headerAction.getIntent();
- if (intent != null && intent.getComponent() != null
- && intent.getComponent().getClassName()
- .equals(mSmartspaceTrampolineActivityComponent)) {
- mActivityStarter.postStartActivityDismissingKeyguard(
- intent, 0 /*delay*/);
- }
- });
- }
- });
- // We need to use an unfiltered listener here since weather is filtered from showing
- // in the dream smartspace.
- mSmartSpaceController.addUnfilteredListener(mSmartspaceTargetListener);
- }
-
- @Override
- protected void onViewDetached() {
- mSmartSpaceController.removeUnfilteredListener(mSmartspaceTargetListener);
- }
-
- /**
- * Returns a formatted content description for accessibility of the weather condition and
- * temperature.
- */
- private CharSequence getFormattedContentDescription(CharSequence temperature,
- CharSequence weatherCondition) {
- if (TextUtils.isEmpty(temperature)) {
- return weatherCondition;
- } else if (TextUtils.isEmpty(weatherCondition)) {
- return temperature;
- }
-
- return mResources.getString(R.string.dream_overlay_weather_complication_desc,
- weatherCondition, temperature);
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamAirQualityComplicationComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamAirQualityComplicationComponent.java
deleted file mode 100644
index 112a1ce..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamAirQualityComplicationComponent.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.dreams.complication.dagger;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import androidx.annotation.ColorInt;
-
-import com.android.systemui.R;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dreams.complication.ComplicationLayoutParams;
-import com.android.systemui.dreams.complication.DreamAirQualityComplication;
-import com.android.systemui.dreams.complication.DreamAirQualityComplication.DreamAirQualityViewHolder;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Objects;
-
-import javax.inject.Named;
-import javax.inject.Scope;
-
-import dagger.Module;
-import dagger.Provides;
-import dagger.Subcomponent;
-
-/**
- * Component responsible for generating dependencies for the {@link DreamAirQualityComplication},
- * such as the layout details.
- */
-@Subcomponent(modules = {
- DreamAirQualityComplicationComponent.DreamAirQualityComplicationModule.class,
-})
-@DreamAirQualityComplicationComponent.DreamAirQualityComplicationScope
-public interface DreamAirQualityComplicationComponent {
-
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Scope
- @interface DreamAirQualityComplicationScope {
- }
-
- /**
- * Generates {@link DreamAirQualityComplicationComponent}.
- */
- @Subcomponent.Factory
- interface Factory {
- DreamAirQualityComplicationComponent create();
- }
-
- /**
- * Creates {@link DreamAirQualityViewHolder}.
- */
- DreamAirQualityViewHolder getViewHolder();
-
- /**
- * Scoped values for {@link DreamAirQualityComplicationComponent}.
- */
- @Module
- interface DreamAirQualityComplicationModule {
- String DREAM_AQI_COMPLICATION_VIEW = "aqi_complication_view";
- String DREAM_AQI_COMPLICATION_LAYOUT_PARAMS = "aqi_complication_layout_params";
- String DREAM_AQI_COLOR_THRESHOLDS = "aqi_color_thresholds";
- String DREAM_AQI_COLOR_VALUES = "aqi_color_values";
- String DREAM_AQI_COLOR_DEFAULT = "aqi_color_default";
- // Order weight of insert into parent container
- int INSERT_ORDER_WEIGHT = 1;
-
- /**
- * Provides the complication view.
- */
- @Provides
- @DreamAirQualityComplicationScope
- @Named(DREAM_AQI_COMPLICATION_VIEW)
- static TextView provideComplicationView(LayoutInflater layoutInflater) {
- return Objects.requireNonNull((TextView)
- layoutInflater.inflate(R.layout.dream_overlay_complication_aqi,
- null, false),
- "R.layout.dream_overlay_complication_aqi did not properly inflated");
- }
-
- /**
- * Provides the layout parameters for the complication view.
- */
- @Provides
- @DreamAirQualityComplicationScope
- @Named(DREAM_AQI_COMPLICATION_LAYOUT_PARAMS)
- static ComplicationLayoutParams provideLayoutParams() {
- return new ComplicationLayoutParams(0,
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ComplicationLayoutParams.POSITION_BOTTOM
- | ComplicationLayoutParams.POSITION_START,
- ComplicationLayoutParams.DIRECTION_END,
- INSERT_ORDER_WEIGHT, /* snapToGuide= */ true);
- }
-
- @Provides
- @DreamAirQualityComplicationScope
- @Named(DREAM_AQI_COLOR_THRESHOLDS)
- static int[] provideAqiColorThresholds(@Main Resources resources) {
- return resources.getIntArray(R.array.config_dreamAqiThresholds);
- }
-
- @Provides
- @DreamAirQualityComplicationScope
- @Named(DREAM_AQI_COLOR_VALUES)
- static int[] provideAqiColorValues(@Main Resources resources) {
- return resources.getIntArray(R.array.config_dreamAqiColorValues);
- }
-
- @Provides
- @DreamAirQualityComplicationScope
- @Named(DREAM_AQI_COLOR_DEFAULT)
- @ColorInt
- static int provideDefaultAqiColor(Context context) {
- return context.getColor(R.color.dream_overlay_aqi_unknown);
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamWeatherComplicationComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamWeatherComplicationComponent.java
deleted file mode 100644
index f1a1689..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamWeatherComplicationComponent.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.dreams.complication.dagger;
-
-
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import android.content.res.Resources;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.android.internal.util.Preconditions;
-import com.android.systemui.R;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dreams.complication.ComplicationLayoutParams;
-import com.android.systemui.dreams.complication.DreamWeatherComplication.DreamWeatherViewHolder;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-
-import javax.inject.Named;
-import javax.inject.Scope;
-
-import dagger.Binds;
-import dagger.Module;
-import dagger.Provides;
-import dagger.Subcomponent;
-
-/**
- * {@link DreamWeatherComplicationComponent} is responsible for generating dependencies surrounding
- * the
- * Clock Date {@link com.android.systemui.dreams.complication.Complication}, such as the layout
- * details.
- */
-@Subcomponent(modules = {
- DreamWeatherComplicationComponent.DreamWeatherComplicationModule.class,
-})
-@DreamWeatherComplicationComponent.DreamWeatherComplicationScope
-public interface DreamWeatherComplicationComponent {
- /**
- * Creates {@link DreamWeatherViewHolder}.
- */
- DreamWeatherViewHolder getViewHolder();
-
- @Documented
- @Retention(RUNTIME)
- @Scope
- @interface DreamWeatherComplicationScope {
- }
-
- /**
- * Generates {@link DreamWeatherComplicationComponent}.
- */
- @Subcomponent.Factory
- interface Factory {
- DreamWeatherComplicationComponent create();
- }
-
- /**
- * Scoped values for {@link DreamWeatherComplicationComponent}.
- */
- @Module
- interface DreamWeatherComplicationModule {
- String DREAM_WEATHER_COMPLICATION_VIEW = "weather_complication_view";
- String DREAM_WEATHER_COMPLICATION_LAYOUT_PARAMS =
- "weather_complication_layout_params";
- // Order weight of insert into parent container
- int INSERT_ORDER_WEIGHT = 2;
-
- /**
- * Provides the complication view.
- */
- @Provides
- @DreamWeatherComplicationScope
- @Named(DREAM_WEATHER_COMPLICATION_VIEW)
- static TextView provideComplicationView(LayoutInflater layoutInflater) {
- return Preconditions.checkNotNull((TextView)
- layoutInflater.inflate(R.layout.dream_overlay_complication_weather,
- null, false),
- "R.layout.dream_overlay_complication_weather did not properly inflated");
- }
-
- /**
- * Provides the layout parameters for the complication view.
- */
- @Provides
- @DreamWeatherComplicationScope
- @Named(DREAM_WEATHER_COMPLICATION_LAYOUT_PARAMS)
- static ComplicationLayoutParams provideLayoutParams() {
- return new ComplicationLayoutParams(0,
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ComplicationLayoutParams.POSITION_BOTTOM
- | ComplicationLayoutParams.POSITION_START,
- ComplicationLayoutParams.DIRECTION_END,
- INSERT_ORDER_WEIGHT, /* snapToGuide= */ true);
- }
-
- /**
- * Binds resources in the dream weather complication scope.
- */
- @Binds
- @DreamWeatherComplicationScope
- Resources getResources(@Main Resources resources);
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
index 98344aa..e45437d 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
@@ -16,15 +16,9 @@
package com.android.systemui.dreams.complication.dagger;
-import android.content.Context;
-
-import com.android.systemui.R;
import com.android.systemui.dagger.SystemUIBinder;
-import javax.inject.Named;
-
import dagger.Module;
-import dagger.Provides;
/**
* Module for all components with corresponding dream layer complications registered in
@@ -33,20 +27,6 @@
@Module(includes = {
DreamClockDateComplicationModule.class,
DreamClockTimeComplicationModule.class,
- },
- subcomponents = {
- DreamWeatherComplicationComponent.class,
- DreamAirQualityComplicationComponent.class,
})
public interface RegisteredComplicationsModule {
- String SMARTSPACE_TRAMPOLINE_ACTIVITY_COMPONENT = "smartspace_trampoline_activity";
-
- /**
- * Provides the smartspace trampoline activity component.
- */
- @Provides
- @Named(SMARTSPACE_TRAMPOLINE_ACTIVITY_COMPONENT)
- static String provideSmartspaceTrampolineActivityComponent(Context context) {
- return context.getString(R.string.config_smartspaceTrampolineActivityComponent);
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleModule.kt b/packages/SystemUI/src/com/android/systemui/people/PeopleModule.kt
new file mode 100644
index 0000000..dd35445
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleModule.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.people
+
+import com.android.systemui.people.data.repository.PeopleTileRepository
+import com.android.systemui.people.data.repository.PeopleTileRepositoryImpl
+import com.android.systemui.people.data.repository.PeopleWidgetRepository
+import com.android.systemui.people.data.repository.PeopleWidgetRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+/** Dagger module to provide/bind people space dependencies. */
+@Module
+interface PeopleModule {
+ @Binds fun bindTileRepository(impl: PeopleTileRepositoryImpl): PeopleTileRepository
+
+ @Binds fun bindWidgetRepository(impl: PeopleWidgetRepositoryImpl): PeopleWidgetRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
index 93a3f81..e845aa8 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
@@ -19,144 +19,52 @@
import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
-import static com.android.systemui.people.PeopleTileViewHelper.getPersonIconBitmap;
-import static com.android.systemui.people.PeopleTileViewHelper.getSizeInDp;
-
-import android.app.Activity;
-import android.app.people.PeopleSpaceTile;
-import android.content.Context;
import android.content.Intent;
-import android.content.res.TypedArray;
-import android.graphics.Color;
-import android.graphics.Outline;
-import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
-import android.widget.LinearLayout;
-import com.android.systemui.R;
-import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
-import com.android.systemui.people.widget.PeopleTileKey;
+import androidx.activity.ComponentActivity;
+import androidx.lifecycle.ViewModelProvider;
-import java.util.ArrayList;
-import java.util.List;
+import com.android.systemui.people.ui.view.PeopleViewBinder;
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel;
import javax.inject.Inject;
/** People Tile Widget configuration activity that shows the user their conversation tiles. */
-public class PeopleSpaceActivity extends Activity {
+public class PeopleSpaceActivity extends ComponentActivity {
private static final String TAG = "PeopleSpaceActivity";
private static final boolean DEBUG = PeopleSpaceUtils.DEBUG;
- private PeopleSpaceWidgetManager mPeopleSpaceWidgetManager;
- private Context mContext;
- private int mAppWidgetId;
+ private final PeopleViewModel.Factory mViewModelFactory;
+ private PeopleViewModel mViewModel;
@Inject
- public PeopleSpaceActivity(PeopleSpaceWidgetManager peopleSpaceWidgetManager) {
+ public PeopleSpaceActivity(PeopleViewModel.Factory viewModelFactory) {
super();
- mPeopleSpaceWidgetManager = peopleSpaceWidgetManager;
-
+ mViewModelFactory = viewModelFactory;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- mContext = getApplicationContext();
- mAppWidgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID,
- INVALID_APPWIDGET_ID);
setResult(RESULT_CANCELED);
- }
+ mViewModel = new ViewModelProvider(this, mViewModelFactory).get(PeopleViewModel.class);
- /** Builds the conversation selection activity. */
- private void buildActivity() {
- List<PeopleSpaceTile> priorityTiles = new ArrayList<>();
- List<PeopleSpaceTile> recentTiles = new ArrayList<>();
- try {
- priorityTiles = mPeopleSpaceWidgetManager.getPriorityTiles();
- recentTiles = mPeopleSpaceWidgetManager.getRecentTiles();
- } catch (Exception e) {
- Log.e(TAG, "Couldn't retrieve conversations", e);
- }
+ // Update the widget ID coming from the intent.
+ int widgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
+ mViewModel.onWidgetIdChanged(widgetId);
- // If no conversations, render activity without conversations
- if (recentTiles.isEmpty() && priorityTiles.isEmpty()) {
- setContentView(R.layout.people_space_activity_no_conversations);
-
- // The Tile preview has colorBackground as its background. Change it so it's different
- // than the activity's background.
- LinearLayout item = findViewById(android.R.id.background);
- GradientDrawable shape = (GradientDrawable) item.getBackground();
- final TypedArray ta = mContext.getTheme().obtainStyledAttributes(
- new int[]{com.android.internal.R.attr.colorSurface});
- shape.setColor(ta.getColor(0, Color.WHITE));
- return;
- }
-
- setContentView(R.layout.people_space_activity);
- setTileViews(R.id.priority, R.id.priority_tiles, priorityTiles);
- setTileViews(R.id.recent, R.id.recent_tiles, recentTiles);
- }
-
- private ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() {
- @Override
- public void getOutline(View view, Outline outline) {
- outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(),
- mContext.getResources().getDimension(R.dimen.people_space_widget_radius));
- }
- };
-
- /** Sets a {@link PeopleSpaceTileView}s for each conversation. */
- private void setTileViews(int viewId, int tilesId, List<PeopleSpaceTile> tiles) {
- if (tiles.isEmpty()) {
- LinearLayout view = findViewById(viewId);
- view.setVisibility(View.GONE);
- return;
- }
-
- ViewGroup layout = findViewById(tilesId);
- layout.setClipToOutline(true);
- layout.setOutlineProvider(mViewOutlineProvider);
- for (int i = 0; i < tiles.size(); ++i) {
- PeopleSpaceTile tile = tiles.get(i);
- PeopleSpaceTileView tileView = new PeopleSpaceTileView(mContext,
- layout, tile.getId(), i == (tiles.size() - 1));
- setTileView(tileView, tile);
- }
- }
-
- /** Sets {@code tileView} with the data in {@code conversation}. */
- private void setTileView(PeopleSpaceTileView tileView, PeopleSpaceTile tile) {
- try {
- if (tile.getUserName() != null) {
- tileView.setName(tile.getUserName().toString());
- }
- tileView.setPersonIcon(getPersonIconBitmap(mContext, tile,
- getSizeInDp(mContext, R.dimen.avatar_size_for_medium,
- mContext.getResources().getDisplayMetrics().density)));
-
- PeopleTileKey key = new PeopleTileKey(tile);
- tileView.setOnClickListener(v -> storeWidgetConfiguration(tile, key));
- } catch (Exception e) {
- Log.e(TAG, "Couldn't retrieve shortcut information", e);
- }
- }
-
- /** Stores the user selected configuration for {@code mAppWidgetId}. */
- private void storeWidgetConfiguration(PeopleSpaceTile tile, PeopleTileKey key) {
- if (PeopleSpaceUtils.DEBUG) {
- if (DEBUG) {
- Log.d(TAG, "Put " + tile.getUserName() + "'s shortcut ID: "
- + tile.getId() + " for widget ID: "
- + mAppWidgetId);
- }
- }
- mPeopleSpaceWidgetManager.addNewWidget(mAppWidgetId, key);
- finishActivity();
+ ViewGroup view = PeopleViewBinder.create(this);
+ PeopleViewBinder.bind(view, mViewModel, /* lifecycleOwner= */ this,
+ () -> {
+ finishActivity();
+ return null;
+ });
+ setContentView(view);
}
/** Finish activity with a successful widget configuration result. */
@@ -169,19 +77,13 @@
/** Finish activity without choosing a widget. */
public void dismissActivity(View v) {
if (DEBUG) Log.d(TAG, "Activity dismissed with no widgets added!");
+ setResult(RESULT_CANCELED);
finish();
}
private void setActivityResult(int result) {
Intent resultValue = new Intent();
- resultValue.putExtra(EXTRA_APPWIDGET_ID, mAppWidgetId);
+ resultValue.putExtra(EXTRA_APPWIDGET_ID, mViewModel.getAppWidgetId().getValue());
setResult(result, resultValue);
}
-
- @Override
- protected void onResume() {
- super.onResume();
- // Refresh tile views to sync new conversations.
- buildActivity();
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java b/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java
index 4ee951f..58e700f 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleStoryIconFactory.java
@@ -28,6 +28,7 @@
import android.graphics.drawable.Drawable;
import android.util.IconDrawableFactory;
import android.util.Log;
+import android.view.ContextThemeWrapper;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
@@ -52,16 +53,15 @@
PeopleStoryIconFactory(Context context, PackageManager pm,
IconDrawableFactory iconDrawableFactory, int iconSizeDp) {
- context.setTheme(android.R.style.Theme_DeviceDefault_DayNight);
- mIconBitmapSize = (int) (iconSizeDp * context.getResources().getDisplayMetrics().density);
- mDensity = context.getResources().getDisplayMetrics().density;
+ mContext = new ContextThemeWrapper(context, android.R.style.Theme_DeviceDefault_DayNight);
+ mIconBitmapSize = (int) (iconSizeDp * mContext.getResources().getDisplayMetrics().density);
+ mDensity = mContext.getResources().getDisplayMetrics().density;
mIconSize = mDensity * iconSizeDp;
mPackageManager = pm;
mIconDrawableFactory = iconDrawableFactory;
- mImportantConversationColor = context.getColor(R.color.important_conversation);
- mAccentColor = Utils.getColorAttr(context,
+ mImportantConversationColor = mContext.getColor(R.color.important_conversation);
+ mAccentColor = Utils.getColorAttr(mContext,
com.android.internal.R.attr.colorAccentPrimaryVariant).getDefaultColor();
- mContext = context;
}
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java
index 00aa138..be82b1f 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java
@@ -75,6 +75,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
+import com.android.systemui.people.data.model.PeopleTileModel;
import com.android.systemui.people.widget.LaunchConversationActivity;
import com.android.systemui.people.widget.PeopleSpaceWidgetProvider;
import com.android.systemui.people.widget.PeopleTileKey;
@@ -299,7 +300,8 @@
return createLastInteractionRemoteViews();
}
- private static boolean isDndBlockingTileData(@Nullable PeopleSpaceTile tile) {
+ /** Whether the conversation associated with {@code tile} can bypass DND. */
+ public static boolean isDndBlockingTileData(@Nullable PeopleSpaceTile tile) {
if (tile == null) return false;
int notificationPolicyState = tile.getNotificationPolicyState();
@@ -536,7 +538,8 @@
return views;
}
- private static boolean getHasNewStory(PeopleSpaceTile tile) {
+ /** Whether {@code tile} has a new story. */
+ public static boolean getHasNewStory(PeopleSpaceTile tile) {
return tile.getStatuses() != null && tile.getStatuses().stream().anyMatch(
c -> c.getActivity() == ACTIVITY_NEW_STORY);
}
@@ -1250,16 +1253,24 @@
}
/** Returns a bitmap with the user icon and package icon. */
- public static Bitmap getPersonIconBitmap(Context context, PeopleSpaceTile tile,
+ public static Bitmap getPersonIconBitmap(Context context, PeopleTileModel tile,
int maxAvatarSize) {
- boolean hasNewStory = getHasNewStory(tile);
- return getPersonIconBitmap(context, tile, maxAvatarSize, hasNewStory);
+ return getPersonIconBitmap(context, maxAvatarSize, tile.getHasNewStory(),
+ tile.getUserIcon(), tile.getKey().getPackageName(), tile.getKey().getUserId(),
+ tile.isImportant(), tile.isDndBlocking());
}
/** Returns a bitmap with the user icon and package icon. */
private static Bitmap getPersonIconBitmap(
Context context, PeopleSpaceTile tile, int maxAvatarSize, boolean hasNewStory) {
- Icon icon = tile.getUserIcon();
+ return getPersonIconBitmap(context, maxAvatarSize, hasNewStory, tile.getUserIcon(),
+ tile.getPackageName(), getUserId(tile),
+ tile.isImportantConversation(), isDndBlockingTileData(tile));
+ }
+
+ private static Bitmap getPersonIconBitmap(
+ Context context, int maxAvatarSize, boolean hasNewStory, Icon icon, String packageName,
+ int userId, boolean importantConversation, boolean dndBlockingTileData) {
if (icon == null) {
Drawable placeholder = context.getDrawable(R.drawable.ic_avatar_with_badge).mutate();
placeholder.setColorFilter(getDisabledColorFilter());
@@ -1272,10 +1283,10 @@
RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create(
context.getResources(), icon.getBitmap());
Drawable personDrawable = storyIcon.getPeopleTileDrawable(roundedDrawable,
- tile.getPackageName(), getUserId(tile), tile.isImportantConversation(),
+ packageName, userId, importantConversation,
hasNewStory);
- if (isDndBlockingTileData(tile)) {
+ if (dndBlockingTileData) {
personDrawable.setColorFilter(getDisabledColorFilter());
}
diff --git a/packages/SystemUI/src/com/android/systemui/people/data/model/PeopleTileModel.kt b/packages/SystemUI/src/com/android/systemui/people/data/model/PeopleTileModel.kt
new file mode 100644
index 0000000..5d8539f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/data/model/PeopleTileModel.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.people.data.model
+
+import android.graphics.drawable.Icon
+import com.android.systemui.people.widget.PeopleTileKey
+
+/** Models a tile/conversation. */
+data class PeopleTileModel(
+ val key: PeopleTileKey,
+ val username: String,
+ val userIcon: Icon,
+ val hasNewStory: Boolean,
+ val isImportant: Boolean,
+ val isDndBlocking: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/people/data/repository/PeopleTileRepository.kt b/packages/SystemUI/src/com/android/systemui/people/data/repository/PeopleTileRepository.kt
new file mode 100644
index 0000000..01b43d5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/data/repository/PeopleTileRepository.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.people.data.repository
+
+import android.app.people.PeopleSpaceTile
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.people.PeopleTileViewHelper
+import com.android.systemui.people.data.model.PeopleTileModel
+import com.android.systemui.people.widget.PeopleSpaceWidgetManager
+import com.android.systemui.people.widget.PeopleTileKey
+import javax.inject.Inject
+
+/** A Repository to fetch the current tiles/conversations. */
+// TODO(b/238993727): Make the tiles API reactive.
+interface PeopleTileRepository {
+ /* The current priority tiles. */
+ fun priorityTiles(): List<PeopleTileModel>
+
+ /* The current recent tiles. */
+ fun recentTiles(): List<PeopleTileModel>
+}
+
+@SysUISingleton
+class PeopleTileRepositoryImpl
+@Inject
+constructor(
+ private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager,
+) : PeopleTileRepository {
+ override fun priorityTiles(): List<PeopleTileModel> {
+ return peopleSpaceWidgetManager.priorityTiles.map { it.toModel() }
+ }
+
+ override fun recentTiles(): List<PeopleTileModel> {
+ return peopleSpaceWidgetManager.recentTiles.map { it.toModel() }
+ }
+
+ private fun PeopleSpaceTile.toModel(): PeopleTileModel {
+ return PeopleTileModel(
+ PeopleTileKey(this),
+ userName.toString(),
+ userIcon,
+ PeopleTileViewHelper.getHasNewStory(this),
+ isImportantConversation,
+ PeopleTileViewHelper.isDndBlockingTileData(this),
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/people/data/repository/PeopleWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/people/data/repository/PeopleWidgetRepository.kt
new file mode 100644
index 0000000..f2b6cb1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/data/repository/PeopleWidgetRepository.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.people.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.people.widget.PeopleSpaceWidgetManager
+import com.android.systemui.people.widget.PeopleTileKey
+import javax.inject.Inject
+
+interface PeopleWidgetRepository {
+ /**
+ * Bind the widget with ID [widgetId] to the tile keyed by [tileKey].
+ *
+ * If there is already a widget with [widgetId], this existing widget will be reconfigured and
+ * associated to this tile. If there is no widget with [widgetId], a new one will be created.
+ */
+ fun setWidgetTile(widgetId: Int, tileKey: PeopleTileKey)
+}
+
+@SysUISingleton
+class PeopleWidgetRepositoryImpl
+@Inject
+constructor(
+ private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager,
+) : PeopleWidgetRepository {
+ override fun setWidgetTile(widgetId: Int, tileKey: PeopleTileKey) {
+ peopleSpaceWidgetManager.addNewWidget(widgetId, tileKey)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt b/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt
new file mode 100644
index 0000000..bc982cc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.people.ui.view
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Outline
+import android.graphics.drawable.GradientDrawable
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewOutlineProvider
+import android.widget.LinearLayout
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.Lifecycle.State.CREATED
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.people.PeopleSpaceTileView
+import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+/** A ViewBinder for [PeopleViewModel]. */
+object PeopleViewBinder {
+ private const val TAG = "PeopleSpaceViewBinder"
+
+ /**
+ * The [ViewOutlineProvider] used to clip the corner radius of the recent and priority lists.
+ */
+ private val ViewOutlineProvider =
+ object : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ outline.setRoundRect(
+ 0,
+ 0,
+ view.width,
+ view.height,
+ view.context.resources.getDimension(R.dimen.people_space_widget_radius),
+ )
+ }
+ }
+
+ /** Create a [View] that can later be [bound][bind] to a [PeopleViewModel]. */
+ @JvmStatic
+ fun create(context: Context): ViewGroup {
+ return LayoutInflater.from(context)
+ .inflate(R.layout.people_space_activity, /* root= */ null) as ViewGroup
+ }
+
+ /** Bind [view] to [viewModel]. */
+ @JvmStatic
+ fun bind(
+ view: ViewGroup,
+ viewModel: PeopleViewModel,
+ lifecycleOwner: LifecycleOwner,
+ onFinish: () -> Unit,
+ ) {
+ // Call [onFinish] this activity when the ViewModel tells us so.
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(CREATED) {
+ viewModel.isFinished.collect { isFinished ->
+ if (isFinished) {
+ viewModel.clearIsFinished()
+ onFinish()
+ }
+ }
+ }
+ }
+
+ // Start collecting the UI data once the Activity is STARTED.
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ combine(
+ viewModel.priorityTiles,
+ viewModel.recentTiles,
+ ) { priority, recent ->
+ priority to recent
+ }
+ .collect { (priorityTiles, recentTiles) ->
+ if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) {
+ setConversationsContent(
+ view,
+ priorityTiles,
+ recentTiles,
+ viewModel::onTileClicked,
+ )
+ } else {
+ setNoConversationsContent(view)
+ }
+ }
+ }
+ }
+
+ // Make sure to refresh the tiles/conversations when the Activity is resumed, so that it
+ // updates them when going back to the Activity after leaving it.
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ viewModel.onTileRefreshRequested()
+ }
+ }
+ }
+
+ private fun setNoConversationsContent(view: ViewGroup) {
+ // This should never happen.
+ if (view.childCount > 1) {
+ error("view has ${view.childCount} children, it should have maximum 1")
+ }
+
+ // The static content for no conversations is already shown.
+ if (view.findViewById<View>(R.id.top_level_no_conversations) != null) {
+ return
+ }
+
+ // If we were showing the content with conversations earlier, remove it.
+ if (view.childCount == 1) {
+ view.removeViewAt(0)
+ }
+
+ val context = view.context
+ val noConversationsView =
+ LayoutInflater.from(context)
+ .inflate(R.layout.people_space_activity_no_conversations, /* root= */ view)
+
+ // The Tile preview has colorBackground as its background. Change it so it's different than
+ // the activity's background.
+ val item = noConversationsView.findViewById<LinearLayout>(android.R.id.background)
+ val shape = item.background as GradientDrawable
+ val ta =
+ context.theme.obtainStyledAttributes(
+ intArrayOf(com.android.internal.R.attr.colorSurface)
+ )
+ shape.setColor(ta.getColor(0, Color.WHITE))
+ ta.recycle()
+ }
+
+ private fun setConversationsContent(
+ view: ViewGroup,
+ priorityTiles: List<PeopleTileViewModel>,
+ recentTiles: List<PeopleTileViewModel>,
+ onTileClicked: (PeopleTileViewModel) -> Unit,
+ ) {
+ // This should never happen.
+ if (view.childCount > 1) {
+ error("view has ${view.childCount} children, it should have maximum 1")
+ }
+
+ // Inflate the content with conversations, if it's not already.
+ if (view.findViewById<View>(R.id.top_level_with_conversations) == null) {
+ // If we were showing the content without conversations earlier, remove it.
+ if (view.childCount == 1) {
+ view.removeViewAt(0)
+ }
+
+ LayoutInflater.from(view.context)
+ .inflate(R.layout.people_space_activity_with_conversations, /* root= */ view)
+ }
+
+ // TODO(b/193782241): Replace the NestedScrollView + 2x LinearLayout from this layout into a
+ // single RecyclerView once this screen is tested by screenshot tests. Introduce a
+ // PeopleSpaceTileViewBinder that will properly create and bind the View associated to a
+ // PeopleSpaceTileViewModel (and remove the PeopleSpaceTileView class).
+ val conversationsView = view.requireViewById<View>(R.id.top_level_with_conversations)
+ setTileViews(
+ conversationsView,
+ R.id.priority,
+ R.id.priority_tiles,
+ priorityTiles,
+ onTileClicked,
+ )
+
+ setTileViews(
+ conversationsView,
+ R.id.recent,
+ R.id.recent_tiles,
+ recentTiles,
+ onTileClicked,
+ )
+ }
+
+ /** Sets a [PeopleSpaceTileView]s for each conversation. */
+ private fun setTileViews(
+ root: View,
+ tilesListId: Int,
+ tilesId: Int,
+ tiles: List<PeopleTileViewModel>,
+ onTileClicked: (PeopleTileViewModel) -> Unit,
+ ) {
+ // Remove any previously added tile.
+ // TODO(b/193782241): Once this list is a big RecyclerView, set the current list and use
+ // DiffUtil to do as less addView/removeView as possible.
+ val layout = root.requireViewById<ViewGroup>(tilesId)
+ layout.removeAllViews()
+ layout.outlineProvider = ViewOutlineProvider
+
+ val tilesListView = root.requireViewById<LinearLayout>(tilesListId)
+ if (tiles.isEmpty()) {
+ tilesListView.visibility = View.GONE
+ return
+ }
+ tilesListView.visibility = View.VISIBLE
+
+ // Add each tile.
+ tiles.forEachIndexed { i, tile ->
+ val tileView =
+ PeopleSpaceTileView(root.context, layout, tile.key.shortcutId, i == tiles.size - 1)
+ bindTileView(tileView, tile, onTileClicked)
+ }
+ }
+
+ /** Sets [tileView] with the data in [conversation]. */
+ private fun bindTileView(
+ tileView: PeopleSpaceTileView,
+ tile: PeopleTileViewModel,
+ onTileClicked: (PeopleTileViewModel) -> Unit,
+ ) {
+ try {
+ tileView.setName(tile.username)
+ tileView.setPersonIcon(tile.icon)
+ tileView.setOnClickListener { onTileClicked(tile) }
+ } catch (e: Exception) {
+ Log.e(TAG, "Couldn't retrieve shortcut information", e)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleTileViewModel.kt
new file mode 100644
index 0000000..40205ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleTileViewModel.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.people.ui.viewmodel
+
+import android.graphics.Bitmap
+import com.android.systemui.people.widget.PeopleTileKey
+
+/** Models UI state for a single tile/conversation. */
+data class PeopleTileViewModel(
+ val key: PeopleTileKey,
+ val icon: Bitmap,
+ val username: String?,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
new file mode 100644
index 0000000..17de991
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.people.ui.viewmodel
+
+import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
+import android.content.Context
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.people.PeopleSpaceUtils
+import com.android.systemui.people.PeopleTileViewHelper
+import com.android.systemui.people.data.model.PeopleTileModel
+import com.android.systemui.people.data.repository.PeopleTileRepository
+import com.android.systemui.people.data.repository.PeopleWidgetRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ * Models UI state for the people space, allowing the user to select which conversation should be
+ * associated to a new or existing Conversation widget.
+ */
+class PeopleViewModel(
+ @Application private val context: Context,
+ private val tileRepository: PeopleTileRepository,
+ private val widgetRepository: PeopleWidgetRepository,
+) : ViewModel() {
+ /**
+ * The list of the priority tiles/conversations.
+ *
+ * Important: Even though this is a Flow, the underlying API used to populate this Flow is not
+ * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
+ */
+ private val _priorityTiles = MutableStateFlow(priorityTiles())
+ val priorityTiles: Flow<List<PeopleTileViewModel>> = _priorityTiles
+
+ /**
+ * The list of the priority tiles/conversations.
+ *
+ * Important: Even though this is a Flow, the underlying API used to populate this Flow is not
+ * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
+ */
+ private val _recentTiles = MutableStateFlow(recentTiles())
+ val recentTiles: Flow<List<PeopleTileViewModel>> = _recentTiles
+
+ /** The ID of the widget currently being edited/added. */
+ private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)
+ val appWidgetId: StateFlow<Int> = _appWidgetId
+
+ /** Whether the user journey is complete. */
+ private val _isFinished = MutableStateFlow(false)
+ val isFinished: StateFlow<Boolean> = _isFinished
+
+ /** Refresh the [priorityTiles] and [recentTiles]. */
+ fun onTileRefreshRequested() {
+ _priorityTiles.value = priorityTiles()
+ _recentTiles.value = recentTiles()
+ }
+
+ /** Called when the [appWidgetId] should be changed to [widgetId]. */
+ fun onWidgetIdChanged(widgetId: Int) {
+ _appWidgetId.value = widgetId
+ }
+
+ /** Clear [isFinished], setting it to false. */
+ fun clearIsFinished() {
+ _isFinished.value = false
+ }
+
+ /** Called when a tile is clicked. */
+ fun onTileClicked(tile: PeopleTileViewModel) {
+ if (PeopleSpaceUtils.DEBUG) {
+ Log.d(
+ TAG,
+ "Put ${tile.username}'s shortcut ID: ${tile.key.shortcutId} for widget ID: " +
+ _appWidgetId.value
+ )
+ }
+ widgetRepository.setWidgetTile(_appWidgetId.value, tile.key)
+ _isFinished.value = true
+ }
+
+ private fun priorityTiles(): List<PeopleTileViewModel> {
+ return try {
+ tileRepository.priorityTiles().map { it.toViewModel() }
+ } catch (e: Exception) {
+ Log.e(TAG, "Couldn't retrieve priority conversations", e)
+ emptyList()
+ }
+ }
+
+ private fun recentTiles(): List<PeopleTileViewModel> {
+ return try {
+ tileRepository.recentTiles().map { it.toViewModel() }
+ } catch (e: Exception) {
+ Log.e(TAG, "Couldn't retrieve recent conversations", e)
+ emptyList()
+ }
+ }
+
+ private fun PeopleTileModel.toViewModel(): PeopleTileViewModel {
+ val icon =
+ PeopleTileViewHelper.getPersonIconBitmap(
+ context,
+ this,
+ PeopleTileViewHelper.getSizeInDp(
+ context,
+ R.dimen.avatar_size_for_medium,
+ context.resources.displayMetrics.density,
+ )
+ )
+ return PeopleTileViewModel(key, icon, username)
+ }
+
+ /** The Factory that should be used to create a [PeopleViewModel]. */
+ class Factory
+ @Inject
+ constructor(
+ @Application private val context: Context,
+ private val tileRepository: PeopleTileRepository,
+ private val widgetRepository: PeopleWidgetRepository,
+ ) : ViewModelProvider.Factory {
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ check(modelClass == PeopleViewModel::class.java)
+ return PeopleViewModel(context, tileRepository, widgetRepository) as T
+ }
+ }
+
+ companion object {
+ private const val TAG = "PeopleSpaceViewModel"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java b/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java
index e1b97a4..20c6c556 100644
--- a/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java
@@ -21,6 +21,7 @@
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.os.Bundle;
+import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.UserManager;
@@ -34,6 +35,7 @@
import com.android.internal.logging.UiEventLoggerImpl;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.people.PeopleSpaceUtils;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -43,6 +45,7 @@
import com.android.wm.shell.bubbles.Bubble;
import java.util.Optional;
+import java.util.concurrent.Executor;
import javax.inject.Inject;
@@ -58,6 +61,7 @@
private boolean mIsForTesting;
private IStatusBarService mIStatusBarService;
private CommandQueue mCommandQueue;
+ private Executor mBgExecutor;
private Bubble mBubble;
private NotificationEntry mEntryToBubble;
@@ -67,7 +71,8 @@
CommonNotifCollection commonNotifCollection,
Optional<BubblesManager> bubblesManagerOptional,
UserManager userManager,
- CommandQueue commandQueue
+ CommandQueue commandQueue,
+ @Background Executor bgExecutor
) {
super();
mVisibilityProvider = visibilityProvider;
@@ -91,6 +96,7 @@
mCommandQueue.removeCallback(this);
}
});
+ mBgExecutor = bgExecutor;
}
@Override
@@ -172,34 +178,36 @@
return;
}
- try {
- if (mIStatusBarService == null || mCommonNotifCollection == null) {
- if (DEBUG) {
- Log.d(TAG, "Skipping clear notification: null services, key: " + notifKey);
- }
- return;
+ if (mIStatusBarService == null || mCommonNotifCollection == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Skipping clear notification: null services, key: " + notifKey);
}
-
- NotificationEntry entry = mCommonNotifCollection.getEntry(notifKey);
- if (entry == null || entry.getRanking() == null) {
- if (DEBUG) {
- Log.d(TAG, "Skipping clear notification: NotificationEntry or its Ranking"
- + " is null, key: " + notifKey);
- }
- return;
- }
-
- NotificationVisibility notifVisibility = mVisibilityProvider.obtain(entry, true);
- int rank = notifVisibility.rank;
-
- if (DEBUG) Log.d(TAG, "Clearing notification, key: " + notifKey + ", rank: " + rank);
- mIStatusBarService.onNotificationClear(
- packageName, userHandle.getIdentifier(), notifKey,
- NotificationStats.DISMISSAL_OTHER,
- NotificationStats.DISMISS_SENTIMENT_POSITIVE, notifVisibility);
- } catch (Exception e) {
- Log.e(TAG, "Exception cancelling notification:" + e);
+ return;
}
+
+ NotificationEntry entry = mCommonNotifCollection.getEntry(notifKey);
+ if (entry == null || entry.getRanking() == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Skipping clear notification: NotificationEntry or its Ranking"
+ + " is null, key: " + notifKey);
+ }
+ return;
+ }
+
+ NotificationVisibility notifVisibility = mVisibilityProvider.obtain(entry, true);
+ int rank = notifVisibility.rank;
+
+ if (DEBUG) Log.d(TAG, "Clearing notification, key: " + notifKey + ", rank: " + rank);
+ mBgExecutor.execute(() -> {
+ try {
+ mIStatusBarService.onNotificationClear(
+ packageName, userHandle.getIdentifier(), notifKey,
+ NotificationStats.DISMISSAL_OTHER,
+ NotificationStats.DISMISS_SENTIMENT_POSITIVE, notifVisibility);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception cancelling notification:" + e);
+ }
+ });
}
@VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
index 1c4911d..58acfb4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
@@ -7,6 +7,8 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.qs.QS
import com.android.systemui.shade.NotificationPanelViewController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
@@ -27,7 +29,8 @@
private val context: Context,
private val splitShadeOverScrollerFactory: SplitShadeOverScroller.Factory,
private val noOpOverScroller: NoOpOverScroller,
- private val scrimShadeTransitionController: ScrimShadeTransitionController
+ private val scrimShadeTransitionController: ScrimShadeTransitionController,
+ private val statusBarStateController: SysuiStatusBarStateController,
) {
lateinit var notificationPanelViewController: NotificationPanelViewController
@@ -43,7 +46,7 @@
}
private val shadeOverScroller: ShadeOverScroller
get() =
- if (inSplitShade && propertiesInitialized()) {
+ if (inSplitShade && isScreenUnlocked() && propertiesInitialized()) {
splitShadeOverScroller
} else {
noOpOverScroller
@@ -90,6 +93,7 @@
"""
ShadeTransitionController:
inSplitShade: $inSplitShade
+ isScreenUnlocked: ${isScreenUnlocked()}
currentPanelState: ${currentPanelState?.panelStateToString()}
lastPanelExpansionChangeEvent: $lastPanelExpansionChangeEvent
qs.isInitialized: ${this::qs.isInitialized}
@@ -97,4 +101,7 @@
nssl.isInitialized: ${this::notificationStackScrollLayoutController.isInitialized}
""".trimIndent())
}
+
+ private fun isScreenUnlocked() =
+ statusBarStateController.currentOrUpcomingState == StatusBarState.SHADE
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 587c23b..9e77dbc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -68,6 +68,7 @@
import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
+import com.android.systemui.statusbar.phone.StatusBarIconList;
import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallFlags;
@@ -256,6 +257,16 @@
*/
@Provides
@SysUISingleton
+ static StatusBarIconList provideStatusBarIconList(Context context) {
+ return new StatusBarIconList(
+ context.getResources().getStringArray(
+ com.android.internal.R.array.config_statusBarIcons));
+ }
+
+ /**
+ */
+ @Provides
+ @SysUISingleton
static OngoingCallController provideOngoingCallController(
Context context,
CommonNotifCollection notifCollection,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index c86bf93..852d5f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -41,6 +41,7 @@
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.systemui.Dumpable;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.statusbar.NotificationLifetimeExtender;
import com.android.systemui.statusbar.NotificationListener;
@@ -73,6 +74,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.Executor;
import dagger.Lazy;
@@ -113,6 +115,7 @@
private final IStatusBarService mStatusBarService;
private final NotifLiveDataStoreImpl mNotifLiveDataStore;
private final DumpManager mDumpManager;
+ private final Executor mBgExecutor;
private final Set<NotificationEntry> mAllNotifications = new ArraySet<>();
private final Set<NotificationEntry> mReadOnlyAllNotifications =
@@ -159,7 +162,8 @@
LeakDetector leakDetector,
IStatusBarService statusBarService,
NotifLiveDataStoreImpl notifLiveDataStore,
- DumpManager dumpManager
+ DumpManager dumpManager,
+ @Background Executor bgExecutor
) {
mLogger = logger;
mGroupManager = groupManager;
@@ -170,6 +174,7 @@
mStatusBarService = statusBarService;
mNotifLiveDataStore = notifLiveDataStore;
mDumpManager = dumpManager;
+ mBgExecutor = bgExecutor;
}
/** Once called, the NEM will start processing notification events from system server. */
@@ -566,17 +571,19 @@
private void sendNotificationRemovalToServer(
StatusBarNotification notification,
DismissedByUserStats dismissedByUserStats) {
- try {
- mStatusBarService.onNotificationClear(
- notification.getPackageName(),
- notification.getUser().getIdentifier(),
- notification.getKey(),
- dismissedByUserStats.dismissalSurface,
- dismissedByUserStats.dismissalSentiment,
- dismissedByUserStats.notificationVisibility);
- } catch (RemoteException ex) {
- // system process is dead if we're here.
- }
+ mBgExecutor.execute(() -> {
+ try {
+ mStatusBarService.onNotificationClear(
+ notification.getPackageName(),
+ notification.getUser().getIdentifier(),
+ notification.getKey(),
+ dismissedByUserStats.dismissalSurface,
+ dismissedByUserStats.dismissalSentiment,
+ dismissedByUserStats.notificationVisibility);
+ } catch (RemoteException ex) {
+ // system process is dead if we're here.
+ }
+ });
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 2c85fdd..351a4be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -70,6 +70,7 @@
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.dump.LogBufferEulogizer;
@@ -112,6 +113,7 @@
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
+import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
@@ -146,6 +148,7 @@
private final NotifPipelineFlags mNotifPipelineFlags;
private final NotifCollectionLogger mLogger;
private final Handler mMainHandler;
+ private final Executor mBgExecutor;
private final LogBufferEulogizer mEulogizer;
private final DumpManager mDumpManager;
@@ -174,6 +177,7 @@
NotifPipelineFlags notifPipelineFlags,
NotifCollectionLogger logger,
@Main Handler mainHandler,
+ @Background Executor bgExecutor,
LogBufferEulogizer logBufferEulogizer,
DumpManager dumpManager) {
mStatusBarService = statusBarService;
@@ -181,6 +185,7 @@
mNotifPipelineFlags = notifPipelineFlags;
mLogger = logger;
mMainHandler = mainHandler;
+ mBgExecutor = bgExecutor;
mEulogizer = logBufferEulogizer;
mDumpManager = dumpManager;
}
@@ -294,18 +299,20 @@
entriesToLocallyDismiss.add(entry);
if (!isCanceled(entry)) {
// send message to system server if this notification hasn't already been cancelled
- try {
- mStatusBarService.onNotificationClear(
- entry.getSbn().getPackageName(),
- entry.getSbn().getUser().getIdentifier(),
- entry.getSbn().getKey(),
- stats.dismissalSurface,
- stats.dismissalSentiment,
- stats.notificationVisibility);
- } catch (RemoteException e) {
- // system process is dead if we're here.
- mLogger.logRemoteExceptionOnNotificationClear(entry, e);
- }
+ mBgExecutor.execute(() -> {
+ try {
+ mStatusBarService.onNotificationClear(
+ entry.getSbn().getPackageName(),
+ entry.getSbn().getUser().getIdentifier(),
+ entry.getSbn().getKey(),
+ stats.dismissalSurface,
+ stats.dismissalSentiment,
+ stats.notificationVisibility);
+ } catch (RemoteException e) {
+ // system process is dead if we're here.
+ mLogger.logRemoteExceptionOnNotificationClear(entry, e);
+ }
+ });
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 4956a276..bf08fc7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -128,7 +128,8 @@
LeakDetector leakDetector,
IStatusBarService statusBarService,
NotifLiveDataStoreImpl notifLiveDataStore,
- DumpManager dumpManager) {
+ DumpManager dumpManager,
+ @Background Executor bgExecutor) {
return new NotificationEntryManager(
logger,
groupManager,
@@ -138,7 +139,8 @@
leakDetector,
statusBarService,
notifLiveDataStore,
- dumpManager);
+ dumpManager,
+ bgExecutor);
}
/** Provides an instance of {@link NotificationGutsManager} */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
index 016b388..99d320d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
@@ -19,6 +19,7 @@
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogLevel.DEBUG
import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.log.LogLevel.WARNING
import com.android.systemui.log.dagger.NotificationInterruptLog
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
@@ -211,6 +212,33 @@
})
}
+ fun logNoFullscreen(entry: NotificationEntry, reason: String) {
+ buffer.log(TAG, DEBUG, {
+ str1 = entry.logKey
+ str2 = reason
+ }, {
+ "No FullScreenIntent: $str2: $str1"
+ })
+ }
+
+ fun logNoFullscreenWarning(entry: NotificationEntry, reason: String) {
+ buffer.log(TAG, WARNING, {
+ str1 = entry.logKey
+ str2 = reason
+ }, {
+ "No FullScreenIntent: WARNING: $str2: $str1"
+ })
+ }
+
+ fun logFullscreen(entry: NotificationEntry, reason: String) {
+ buffer.log(TAG, DEBUG, {
+ str1 = entry.logKey
+ str2 = reason
+ }, {
+ "FullScreenIntent: $str2: $str1"
+ })
+ }
+
fun keyguardHideNotification(entry: NotificationEntry) {
buffer.log(TAG, DEBUG, {
str1 = entry.logKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index 2dd95a3..535dc6e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -177,9 +177,69 @@
*/
@Override
public boolean shouldLaunchFullScreenIntentWhenAdded(NotificationEntry entry) {
- return entry.getSbn().getNotification().fullScreenIntent != null
- && (!shouldHeadsUp(entry)
- || mStatusBarStateController.getState() == StatusBarState.KEYGUARD);
+ if (entry.getSbn().getNotification().fullScreenIntent == null) {
+ return false;
+ }
+
+ // Never show FSI when suppressed by DND
+ if (entry.shouldSuppressFullScreenIntent()) {
+ mLogger.logNoFullscreen(entry, "Suppressed by DND");
+ return false;
+ }
+
+ // Never show FSI if importance is not HIGH
+ if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) {
+ mLogger.logNoFullscreen(entry, "Not important enough");
+ return false;
+ }
+
+ // If the notification has suppressive GroupAlertBehavior, block FSI and warn.
+ StatusBarNotification sbn = entry.getSbn();
+ if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
+ // b/231322873: Detect and report an event when a notification has both an FSI and a
+ // suppressive groupAlertBehavior, and now correctly block the FSI from firing.
+ final int uid = entry.getSbn().getUid();
+ android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "groupAlertBehavior");
+ mLogger.logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN");
+ return false;
+ }
+
+ // If the screen is off, then launch the FullScreenIntent
+ if (!mPowerManager.isInteractive()) {
+ mLogger.logFullscreen(entry, "Device is not interactive");
+ return true;
+ }
+
+ // If the device is currently dreaming, then launch the FullScreenIntent
+ if (isDreaming()) {
+ mLogger.logFullscreen(entry, "Device is dreaming");
+ return true;
+ }
+
+ // If the keyguard is showing, then launch the FullScreenIntent
+ if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
+ mLogger.logFullscreen(entry, "Keyguard is showing");
+ return true;
+ }
+
+ // If the notification should HUN, then we don't need FSI
+ if (shouldHeadsUp(entry)) {
+ mLogger.logNoFullscreen(entry, "Expected to HUN");
+ return false;
+ }
+
+ // If the notification won't HUN for some other reason (DND/snooze/etc), launch FSI.
+ mLogger.logFullscreen(entry, "Expected not to HUN");
+ return true;
+ }
+
+ private boolean isDreaming() {
+ try {
+ return mDreamManager.isDreaming();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to query dream manager.", e);
+ return false;
+ }
}
private boolean shouldHeadsUpWhenAwake(NotificationEntry entry) {
@@ -223,13 +283,7 @@
return false;
}
- boolean isDreaming = false;
- try {
- isDreaming = mDreamManager.isDreaming();
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to query dream manager.", e);
- }
- boolean inUse = mPowerManager.isScreenOn() && !isDreaming;
+ boolean inUse = mPowerManager.isScreenOn() && !isDreaming();
if (!inUse) {
mLogger.logNoHeadsUpNotInUse(entry);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index fff7b6a..2d6d846 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -1709,7 +1709,7 @@
// Wrap the animation controller to dismiss the shade and set
// mIsLaunchingActivityOverLockscreen during the animation.
ActivityLaunchAnimator.Controller delegate = wrapAnimationController(
- animationController, dismissShade);
+ animationController, dismissShade, /* isLaunchForActivity= */ true);
controller = new DelegateLaunchAnimatorController(delegate) {
@Override
public void onIntentStarted(boolean willAnimate) {
@@ -2457,7 +2457,7 @@
true /* isActivityIntent */);
ActivityLaunchAnimator.Controller animController =
animationController != null ? wrapAnimationController(animationController,
- dismissShade) : null;
+ dismissShade, /* isLaunchForActivity= */ true) : null;
// If we animate, we will dismiss the shade only once the animation is done. This is taken
// care of by the StatusBarLaunchAnimationController.
@@ -2535,9 +2535,25 @@
willLaunchResolverActivity, deferred /* deferred */, animate);
}
+ /**
+ * Return a {@link ActivityLaunchAnimator.Controller} wrapping {@code animationController} so
+ * that:
+ * - if it launches in the notification shade window and {@code dismissShade} is true, then
+ * the shade will be instantly dismissed at the end of the animation.
+ * - if it launches in status bar window, it will make the status bar window match the device
+ * size during the animation (that way, the animation won't be clipped by the status bar
+ * size).
+ *
+ * @param animationController the controller that is wrapped and will drive the main animation.
+ * @param dismissShade whether the notification shade will be dismissed at the end of the
+ * animation. This is ignored if {@code animationController} is not
+ * animating in the shade window.
+ * @param isLaunchForActivity whether the launch is for an activity.
+ */
@Nullable
private ActivityLaunchAnimator.Controller wrapAnimationController(
- ActivityLaunchAnimator.Controller animationController, boolean dismissShade) {
+ ActivityLaunchAnimator.Controller animationController, boolean dismissShade,
+ boolean isLaunchForActivity) {
View rootView = animationController.getLaunchContainer().getRootView();
Optional<ActivityLaunchAnimator.Controller> controllerFromStatusBar =
@@ -2551,7 +2567,7 @@
// If the view is not in the status bar, then we are animating a view in the shade.
// We have to make sure that we collapse it when the animation ends or is cancelled.
return new StatusBarLaunchAnimatorController(animationController, this,
- true /* isLaunchForActivity */);
+ isLaunchForActivity);
}
return animationController;
@@ -4083,8 +4099,9 @@
// We wrap animationCallback with a StatusBarLaunchAnimatorController so that the
// shade is collapsed after the animation (or when it is cancelled, aborted, etc).
ActivityLaunchAnimator.Controller controller =
- animationController != null ? new StatusBarLaunchAnimatorController(
- animationController, this, intent.isActivity()) : null;
+ animationController != null ? wrapAnimationController(
+ animationController, /* dismissShade= */ true, intent.isActivity())
+ : null;
mActivityLaunchAnimator.startPendingIntentWithAnimation(
controller, animate, intent.getCreatorPackage(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 2dc3261..a2140c6ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License
*/
-
package com.android.systemui.statusbar.phone;
import static com.android.systemui.statusbar.phone.HeadsUpAppearanceController.CONTENT_FADE_DELAY;
@@ -31,6 +30,7 @@
import android.util.Property;
import android.view.ContextThemeWrapper;
import android.view.View;
+import android.view.ViewGroup;
import android.view.animation.Interpolator;
import androidx.annotation.VisibleForTesting;
@@ -40,7 +40,6 @@
import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
-import com.android.systemui.statusbar.AlphaOptimizedFrameLayout;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.notification.stack.AnimationFilter;
import com.android.systemui.statusbar.notification.stack.AnimationProperties;
@@ -54,7 +53,7 @@
* A container for notification icons. It handles overflowing icons properly and positions them
* correctly on the screen.
*/
-public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
+public class NotificationIconContainer extends ViewGroup {
/**
* A float value indicating how much before the overflow start the icons should transform into
* a dot. A value of 0 means that they are exactly at the end and a value of 1 means it starts
@@ -232,6 +231,31 @@
}
@Override
+ public boolean hasOverlappingRendering() {
+ // Does the same as "AlphaOptimizedFrameLayout".
+ return false;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int childCount = getChildCount();
+ final int maxVisibleIcons = getMaxVisibleIcons(childCount);
+ final int width = MeasureSpec.getSize(widthMeasureSpec);
+ final int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED);
+ int totalWidth = (int) (getActualPaddingStart() + getActualPaddingEnd());
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ measureChild(child, childWidthSpec, heightMeasureSpec);
+ if (i <= maxVisibleIcons) {
+ totalWidth += child.getMeasuredWidth();
+ }
+ }
+ final int measuredWidth = resolveSize(totalWidth, widthMeasureSpec);
+ final int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
+ setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+
+ @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
float centerY = getHeight() / 2.0f;
// we layout all our children on the left at the top
@@ -408,8 +432,7 @@
float translationX = getActualPaddingStart();
int firstOverflowIndex = -1;
int childCount = getChildCount();
- int maxVisibleIcons = mOnLockScreen ? MAX_ICONS_ON_AOD :
- mIsStaticLayout ? MAX_STATIC_ICONS : childCount;
+ int maxVisibleIcons = getMaxVisibleIcons(childCount);
float layoutEnd = getLayoutEnd();
mVisualOverflowStart = 0;
mFirstVisibleIconState = null;
@@ -493,6 +516,11 @@
}
}
+ private int getMaxVisibleIcons(int childCount) {
+ return mOnLockScreen ? MAX_ICONS_ON_AOD :
+ mIsStaticLayout ? MAX_STATIC_ICONS : childCount;
+ }
+
private float getLayoutEnd() {
return getActualWidth() - getActualPaddingEnd();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
index 2052ee6..15c6dcf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
@@ -31,7 +31,7 @@
private final float mIconAlphaWhenOpaque;
- private View mLeftSide, mStatusIcons, mBattery;
+ private View mStartSide, mStatusIcons, mBattery;
private Animator mCurrentAnimation;
/**
@@ -41,7 +41,7 @@
super(backgroundView, R.drawable.status_background);
final Resources res = statusBarView.getContext().getResources();
mIconAlphaWhenOpaque = res.getFraction(R.dimen.status_bar_icon_drawing_alpha, 1, 1);
- mLeftSide = statusBarView.findViewById(R.id.status_bar_left_side);
+ mStartSide = statusBarView.findViewById(R.id.status_bar_start_side_except_heads_up);
mStatusIcons = statusBarView.findViewById(R.id.statusIcons);
mBattery = statusBarView.findViewById(R.id.battery);
applyModeBackground(-1, getMode(), false /*animate*/);
@@ -75,7 +75,7 @@
}
private void applyMode(int mode, boolean animate) {
- if (mLeftSide == null) return; // pre-init
+ if (mStartSide == null) return; // pre-init
float newAlpha = getNonBatteryClockAlphaFor(mode);
float newAlphaBC = getBatteryClockAlpha(mode);
if (mCurrentAnimation != null) {
@@ -84,7 +84,7 @@
if (animate) {
AnimatorSet anims = new AnimatorSet();
anims.playTogether(
- animateTransitionTo(mLeftSide, newAlpha),
+ animateTransitionTo(mStartSide, newAlpha),
animateTransitionTo(mStatusIcons, newAlpha),
animateTransitionTo(mBattery, newAlphaBC)
);
@@ -94,7 +94,7 @@
anims.start();
mCurrentAnimation = anims;
} else {
- mLeftSide.setAlpha(newAlpha);
+ mStartSide.setAlpha(newAlpha);
mStatusIcons.setAlpha(newAlpha);
mBattery.setAlpha(newAlphaBC);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index 9da2ef73..f9c4c8f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -21,7 +21,6 @@
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
-
import com.android.systemui.R
import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController
@@ -32,9 +31,7 @@
import com.android.systemui.util.ViewController
import com.android.systemui.util.kotlin.getOrNull
import com.android.systemui.util.view.ViewUtil
-
import java.util.Optional
-
import javax.inject.Inject
import javax.inject.Named
@@ -58,8 +55,8 @@
override fun onViewAttached() {
if (moveFromCenterAnimationController == null) return
- val statusBarLeftSide: View = mView.findViewById(R.id.status_bar_left_side)
- val systemIconArea: ViewGroup = mView.findViewById(R.id.system_icon_area)
+ val statusBarLeftSide: View = mView.findViewById(R.id.status_bar_start_side_except_heads_up)
+ val systemIconArea: ViewGroup = mView.findViewById(R.id.status_bar_end_side_content)
val viewsToAnimate = arrayOf(
statusBarLeftSide,
@@ -126,11 +123,11 @@
class StatusBarViewsCenterProvider : UnfoldMoveFromCenterAnimator.ViewCenterProvider {
override fun getViewCenter(view: View, outPoint: Point) =
when (view.id) {
- R.id.status_bar_left_side -> {
+ R.id.status_bar_start_side_except_heads_up -> {
// items aligned to the start, return start center point
getViewEdgeCenter(view, outPoint, isStart = true)
}
- R.id.system_icon_area -> {
+ R.id.status_bar_end_side_content -> {
// items aligned to the end, return end center point
getViewEdgeCenter(view, outPoint, isStart = false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index a94c2b7..7c31366 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.phone;
+import static com.android.systemui.statusbar.phone.StatusBarIconList.Slot;
+
import android.annotation.NonNull;
import android.content.Context;
import android.graphics.drawable.Icon;
@@ -56,11 +58,12 @@
* registered with it.
*/
@SysUISingleton
-public class StatusBarIconControllerImpl extends StatusBarIconList implements Tunable,
+public class StatusBarIconControllerImpl implements Tunable,
ConfigurationListener, Dumpable, CommandQueue.Callbacks, StatusBarIconController, DemoMode {
private static final String TAG = "StatusBarIconController";
+ private final StatusBarIconList mStatusBarIconList;
private final ArrayList<IconManager> mIconGroups = new ArrayList<>();
private final ArraySet<String> mIconHideList = new ArraySet<>();
@@ -74,15 +77,12 @@
DemoModeController demoModeController,
ConfigurationController configurationController,
TunerService tunerService,
- DumpManager dumpManager) {
- super(context.getResources().getStringArray(
- com.android.internal.R.array.config_statusBarIcons));
- configurationController.addCallback(this);
-
+ DumpManager dumpManager,
+ StatusBarIconList statusBarIconList) {
+ mStatusBarIconList = statusBarIconList;
mContext = context;
- loadDimens();
-
+ configurationController.addCallback(this);
commandQueue.addCallback(this);
tunerService.addTunable(this, ICON_HIDE_LIST);
demoModeController.addCallback(this);
@@ -101,15 +101,14 @@
group.setController(this);
mIconGroups.add(group);
- List<Slot> allSlots = getSlots();
+ List<Slot> allSlots = mStatusBarIconList.getSlots();
for (int i = 0; i < allSlots.size(); i++) {
Slot slot = allSlots.get(i);
List<StatusBarIconHolder> holders = slot.getHolderListInViewOrder();
boolean hidden = mIconHideList.contains(slot.getName());
for (StatusBarIconHolder holder : holders) {
- int tag = holder.getTag();
- int viewIndex = getViewIndex(getSlotIndex(slot.getName()), holder.getTag());
+ int viewIndex = mStatusBarIconList.getViewIndex(slot.getName(), holder.getTag());
group.onIconAdded(viewIndex, slot.getName(), hidden, holder);
}
}
@@ -144,7 +143,7 @@
}
mIconHideList.clear();
mIconHideList.addAll(StatusBarIconController.getIconHideList(mContext, newValue));
- ArrayList<Slot> currentSlots = getSlots();
+ List<Slot> currentSlots = mStatusBarIconList.getSlots();
ArrayMap<Slot, List<StatusBarIconHolder>> slotsToReAdd = new ArrayMap<>();
// This is a little hacky... Peel off all of the holders on all of the slots
@@ -163,17 +162,13 @@
List<StatusBarIconHolder> iconsForSlot = slotsToReAdd.get(item);
if (iconsForSlot == null) continue;
for (StatusBarIconHolder holder : iconsForSlot) {
- setIcon(getSlotIndex(item.getName()), holder);
+ setIcon(item.getName(), holder);
}
}
}
- private void loadDimens() {
- }
-
- private void addSystemIcon(int index, StatusBarIconHolder holder) {
- String slot = getSlotName(index);
- int viewIndex = getViewIndex(index, holder.getTag());
+ private void addSystemIcon(String slot, StatusBarIconHolder holder) {
+ int viewIndex = mStatusBarIconList.getViewIndex(slot, holder.getTag());
boolean hidden = mIconHideList.contains(slot);
mIconGroups.forEach(l -> l.onIconAdded(viewIndex, slot, hidden, holder));
@@ -182,18 +177,17 @@
/** */
@Override
public void setIcon(String slot, int resourceId, CharSequence contentDescription) {
- int index = getSlotIndex(slot);
- StatusBarIconHolder holder = getIcon(index, 0);
+ StatusBarIconHolder holder = mStatusBarIconList.getIconHolder(slot, 0);
if (holder == null) {
StatusBarIcon icon = new StatusBarIcon(UserHandle.SYSTEM, mContext.getPackageName(),
Icon.createWithResource(
mContext, resourceId), 0, 0, contentDescription);
holder = StatusBarIconHolder.fromIcon(icon);
- setIcon(index, holder);
+ setIcon(slot, holder);
} else {
holder.getIcon().icon = Icon.createWithResource(mContext, resourceId);
holder.getIcon().contentDescription = contentDescription;
- handleSet(index, holder);
+ handleSet(slot, holder);
}
}
@@ -203,21 +197,18 @@
*/
@Override
public void setSignalIcon(String slot, WifiIconState state) {
-
- int index = getSlotIndex(slot);
-
if (state == null) {
- removeIcon(index, 0);
+ removeIcon(slot, 0);
return;
}
- StatusBarIconHolder holder = getIcon(index, 0);
+ StatusBarIconHolder holder = mStatusBarIconList.getIconHolder(slot, 0);
if (holder == null) {
holder = StatusBarIconHolder.fromWifiIconState(state);
- setIcon(index, holder);
+ setIcon(slot, holder);
} else {
holder.setWifiState(state);
- handleSet(index, holder);
+ handleSet(slot, holder);
}
}
@@ -229,8 +220,7 @@
*/
@Override
public void setMobileIcons(String slot, List<MobileIconState> iconStates) {
- Slot mobileSlot = getSlot(slot);
- int slotIndex = getSlotIndex(slot);
+ Slot mobileSlot = mStatusBarIconList.getSlot(slot);
// Reverse the sort order to show icons with left to right([Slot1][Slot2]..).
// StatusBarIconList has UI design that first items go to the right of second items.
@@ -241,10 +231,10 @@
StatusBarIconHolder holder = mobileSlot.getHolderForTag(state.subId);
if (holder == null) {
holder = StatusBarIconHolder.fromMobileIconState(state);
- setIcon(slotIndex, holder);
+ setIcon(slot, holder);
} else {
holder.setMobileState(state);
- handleSet(slotIndex, holder);
+ handleSet(slot, holder);
}
}
}
@@ -256,21 +246,19 @@
*/
@Override
public void setCallStrengthIcons(String slot, List<CallIndicatorIconState> states) {
- Slot callStrengthSlot = getSlot(slot);
- int callStrengthSlotIndex = getSlotIndex(slot);
+ Slot callStrengthSlot = mStatusBarIconList.getSlot(slot);
Collections.reverse(states);
for (CallIndicatorIconState state : states) {
if (!state.isNoCalling) {
StatusBarIconHolder holder = callStrengthSlot.getHolderForTag(state.subId);
if (holder == null) {
holder = StatusBarIconHolder.fromCallIndicatorState(mContext, state);
- setIcon(callStrengthSlotIndex, holder);
} else {
holder.setIcon(new StatusBarIcon(UserHandle.SYSTEM, mContext.getPackageName(),
Icon.createWithResource(mContext, state.callStrengthResId), 0, 0,
state.callStrengthDescription));
- setIcon(callStrengthSlotIndex, holder);
}
+ setIcon(slot, holder);
}
setIconVisibility(slot, !state.isNoCalling, state.subId);
}
@@ -283,21 +271,19 @@
*/
@Override
public void setNoCallingIcons(String slot, List<CallIndicatorIconState> states) {
- Slot noCallingSlot = getSlot(slot);
- int noCallingSlotIndex = getSlotIndex(slot);
+ Slot noCallingSlot = mStatusBarIconList.getSlot(slot);
Collections.reverse(states);
for (CallIndicatorIconState state : states) {
if (state.isNoCalling) {
StatusBarIconHolder holder = noCallingSlot.getHolderForTag(state.subId);
if (holder == null) {
holder = StatusBarIconHolder.fromCallIndicatorState(mContext, state);
- setIcon(noCallingSlotIndex, holder);
} else {
holder.setIcon(new StatusBarIcon(UserHandle.SYSTEM, mContext.getPackageName(),
Icon.createWithResource(mContext, state.noCallingResId), 0, 0,
state.noCallingDescription));
- setIcon(noCallingSlotIndex, holder);
}
+ setIcon(slot, holder);
}
setIconVisibility(slot, state.isNoCalling, state.subId);
}
@@ -305,42 +291,31 @@
@Override
public void setExternalIcon(String slot) {
- int viewIndex = getViewIndex(getSlotIndex(slot), 0);
+ int viewIndex = mStatusBarIconList.getViewIndex(slot, 0);
int height = mContext.getResources().getDimensionPixelSize(
R.dimen.status_bar_icon_drawing_size);
mIconGroups.forEach(l -> l.onIconExternal(viewIndex, height));
}
//TODO: remove this (used in command queue and for 3rd party tiles?)
- @Override
public void setIcon(String slot, StatusBarIcon icon) {
- setIcon(getSlotIndex(slot), icon);
- }
-
- /**
- * For backwards compatibility, in the event that someone gives us a slot and a status bar icon
- */
- private void setIcon(int index, StatusBarIcon icon) {
- String slot = getSlotName(index);
if (icon == null) {
removeAllIconsForSlot(slot);
return;
}
StatusBarIconHolder holder = StatusBarIconHolder.fromIcon(icon);
- setIcon(index, holder);
+ setIcon(slot, holder);
}
- /** */
- @Override
- public void setIcon(int index, @NonNull StatusBarIconHolder holder) {
- boolean isNew = getIcon(index, holder.getTag()) == null;
- super.setIcon(index, holder);
+ private void setIcon(String slot, @NonNull StatusBarIconHolder holder) {
+ boolean isNew = mStatusBarIconList.getIconHolder(slot, holder.getTag()) == null;
+ mStatusBarIconList.setIcon(slot, holder);
if (isNew) {
- addSystemIcon(index, holder);
+ addSystemIcon(slot, holder);
} else {
- handleSet(index, holder);
+ handleSet(slot, holder);
}
}
@@ -351,34 +326,33 @@
/** */
public void setIconVisibility(String slot, boolean visibility, int tag) {
- int index = getSlotIndex(slot);
- StatusBarIconHolder holder = getIcon(index, tag);
+ StatusBarIconHolder holder = mStatusBarIconList.getIconHolder(slot, tag);
if (holder == null || holder.isVisible() == visibility) {
return;
}
holder.setVisible(visibility);
- handleSet(index, holder);
+ handleSet(slot, holder);
}
/** */
@Override
public void setIconAccessibilityLiveRegion(String slotName, int accessibilityLiveRegion) {
- Slot slot = getSlot(slotName);
+ Slot slot = mStatusBarIconList.getSlot(slotName);
if (!slot.hasIconsInSlot()) {
return;
}
- int slotIndex = getSlotIndex(slotName);
List<StatusBarIconHolder> iconsToUpdate = slot.getHolderListInViewOrder();
for (StatusBarIconHolder holder : iconsToUpdate) {
- int viewIndex = getViewIndex(slotIndex, holder.getTag());
+ int viewIndex = mStatusBarIconList.getViewIndex(slotName, holder.getTag());
mIconGroups.forEach(l -> l.mGroup.getChildAt(viewIndex)
.setAccessibilityLiveRegion(accessibilityLiveRegion));
}
}
/** */
+ @Override
public void removeIcon(String slot) {
removeAllIconsForSlot(slot);
}
@@ -386,39 +360,34 @@
/** */
@Override
public void removeIcon(String slot, int tag) {
- removeIcon(getSlotIndex(slot), tag);
+ if (mStatusBarIconList.getIconHolder(slot, tag) == null) {
+ return;
+ }
+ int viewIndex = mStatusBarIconList.getViewIndex(slot, tag);
+ mStatusBarIconList.removeIcon(slot, tag);
+ mIconGroups.forEach(l -> l.onRemoveIcon(viewIndex));
}
/** */
@Override
public void removeAllIconsForSlot(String slotName) {
- Slot slot = getSlot(slotName);
+ Slot slot = mStatusBarIconList.getSlot(slotName);
if (!slot.hasIconsInSlot()) {
return;
}
- int slotIndex = getSlotIndex(slotName);
List<StatusBarIconHolder> iconsToRemove = slot.getHolderListInViewOrder();
for (StatusBarIconHolder holder : iconsToRemove) {
- int viewIndex = getViewIndex(slotIndex, holder.getTag());
+ int viewIndex = mStatusBarIconList.getViewIndex(slotName, holder.getTag());
slot.removeForTag(holder.getTag());
mIconGroups.forEach(l -> l.onRemoveIcon(viewIndex));
}
}
- /** */
- @Override
- public void removeIcon(int index, int tag) {
- if (getIcon(index, tag) == null) {
- return;
- }
- super.removeIcon(index, tag);
- int viewIndex = getViewIndex(index, 0);
- mIconGroups.forEach(l -> l.onRemoveIcon(viewIndex));
- }
- private void handleSet(int index, StatusBarIconHolder holder) {
- int viewIndex = getViewIndex(index, holder.getTag());
+
+ private void handleSet(String slotName, StatusBarIconHolder holder) {
+ int viewIndex = mStatusBarIconList.getViewIndex(slotName, holder.getTag());
mIconGroups.forEach(l -> l.onSetIconHolder(viewIndex, holder));
}
@@ -438,7 +407,7 @@
}
}
- super.dump(pw);
+ mStatusBarIconList.dump(pw);
}
/** */
@@ -482,7 +451,6 @@
/** */
@Override
public void onDensityOrFontScaleChanged() {
- loadDimens();
refreshIconGroups();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconList.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconList.java
index c876c32..8800b05 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconList.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconList.java
@@ -25,60 +25,72 @@
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+/** A class holding the list of all the system icons that could be shown in the status bar. */
public class StatusBarIconList {
- private ArrayList<Slot> mSlots = new ArrayList<>();
+ private final ArrayList<Slot> mSlots = new ArrayList<>();
+ private final List<Slot> mViewOnlySlots = Collections.unmodifiableList(mSlots);
public StatusBarIconList(String[] slots) {
final int N = slots.length;
- for (int i=0; i < N; i++) {
+ for (int i = 0; i < N; i++) {
mSlots.add(new Slot(slots[i], null));
}
}
- public int getSlotIndex(String slot) {
- final int N = mSlots.size();
- for (int i=0; i<N; i++) {
- Slot item = mSlots.get(i);
- if (item.getName().equals(slot)) {
- return i;
- }
- }
- // Auto insert new items at the beginning.
- mSlots.add(0, new Slot(slot, null));
- return 0;
+ /** Returns the list of current slots. */
+ public List<Slot> getSlots() {
+ return mViewOnlySlots;
}
- protected ArrayList<Slot> getSlots() {
- return new ArrayList<>(mSlots);
+ /**
+ * Gets the slot with the given {@code name}, or creates a new slot if we don't already have a
+ * slot by that name.
+ *
+ * If a new slot is created, that slot will be inserted at the front of the list.
+ *
+ * TODO(b/237533036): Rename this to getOrCreateSlot to make it more clear that it could create
+ * a new slot. Other methods in this class will also create a new slot if we don't have one,
+ * should those be re-named too?
+ */
+ public Slot getSlot(String name) {
+ return mSlots.get(findOrInsertSlot(name));
}
- protected Slot getSlot(String name) {
- return mSlots.get(getSlotIndex(name));
+ /**
+ * Sets the icon in {@code holder} to be associated with the slot with the given
+ * {@code slotName}.
+ */
+ public void setIcon(String slotName, @NonNull StatusBarIconHolder holder) {
+ mSlots.get(findOrInsertSlot(slotName)).addHolder(holder);
}
- public int size() {
- return mSlots.size();
+ /**
+ * Removes the icon holder that we had associated with {@code slotName}'s slot at the given
+ * {@code tag}.
+ */
+ public void removeIcon(String slotName, int tag) {
+ mSlots.get(findOrInsertSlot(slotName)).removeForTag(tag);
}
- public void setIcon(int index, @NonNull StatusBarIconHolder holder) {
- mSlots.get(index).addHolder(holder);
+ /**
+ * Returns the icon holder currently associated with {@code slotName}'s slot at the given
+ * {@code tag}, or null if we don't have one.
+ */
+ @Nullable
+ public StatusBarIconHolder getIconHolder(String slotName, int tag) {
+ return mSlots.get(findOrInsertSlot(slotName)).getHolderForTag(tag);
}
- public void removeIcon(int index, int tag) {
- mSlots.get(index).removeForTag(tag);
- }
-
- public String getSlotName(int index) {
- return mSlots.get(index).getName();
- }
-
- public StatusBarIconHolder getIcon(int index, int tag) {
- return mSlots.get(index).getHolderForTag(tag);
- }
-
- public int getViewIndex(int slotIndex, int tag) {
+ /**
+ * Returns the index of the icon in {@code slotName}'s slot at the given {@code tag}.
+ *
+ * Note that a single slot can have multiple icons, and this function takes that into account.
+ */
+ public int getViewIndex(String slotName, int tag) {
+ int slotIndex = findOrInsertSlot(slotName);
int count = 0;
for (int i = 0; i < slotIndex; i++) {
Slot item = mSlots.get(i);
@@ -100,6 +112,25 @@
}
}
+ private int findOrInsertSlot(String slot) {
+ final int N = mSlots.size();
+ for (int i = 0; i < N; i++) {
+ Slot item = mSlots.get(i);
+ if (item.getName().equals(slot)) {
+ return i;
+ }
+ }
+ // Auto insert new items at the beginning.
+ mSlots.add(0, new Slot(slot, null));
+ return 0;
+ }
+
+
+ /**
+ * A class representing one slot in the status bar system icons view.
+ *
+ * Note that one slot can have multiple icons associated with it.
+ */
public static class Slot {
private final String mName;
private StatusBarIconHolder mHolder;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index 0e7da81..39bf92a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -102,7 +102,7 @@
private final KeyguardStateController mKeyguardStateController;
private final NotificationPanelViewController mNotificationPanelViewController;
private final NetworkController mNetworkController;
- private LinearLayout mSystemIconArea;
+ private LinearLayout mEndSideContent;
private View mClockView;
private View mOngoingCallChip;
private View mNotificationIconAreaInner;
@@ -232,16 +232,16 @@
mDarkIconManager.setShouldLog(true);
updateBlockedIcons();
mStatusBarIconController.addIconGroup(mDarkIconManager);
- mSystemIconArea = mStatusBar.findViewById(R.id.system_icon_area);
+ mEndSideContent = mStatusBar.findViewById(R.id.status_bar_end_side_content);
mClockView = mStatusBar.findViewById(R.id.clock);
mOngoingCallChip = mStatusBar.findViewById(R.id.ongoing_call_chip);
- showSystemIconArea(false);
+ showEndSideContent(false);
showClock(false);
initEmergencyCryptkeeperText();
initOperatorName();
initNotificationIconArea();
mSystemEventAnimator =
- new StatusBarSystemEventAnimator(mSystemIconArea, getResources());
+ new StatusBarSystemEventAnimator(mEndSideContent, getResources());
mCarrierConfigTracker.addCallback(mCarrierConfigCallback);
mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
}
@@ -370,10 +370,10 @@
mDisabled2 = state2;
if ((diff1 & DISABLE_SYSTEM_INFO) != 0 || ((diff2 & DISABLE2_SYSTEM_ICONS) != 0)) {
if ((state1 & DISABLE_SYSTEM_INFO) != 0 || ((state2 & DISABLE2_SYSTEM_ICONS) != 0)) {
- hideSystemIconArea(animate);
+ hideEndSideContent(animate);
hideOperatorName(animate);
} else {
- showSystemIconArea(animate);
+ showEndSideContent(animate);
showOperatorName(animate);
}
}
@@ -474,15 +474,15 @@
return mStatusBarHideIconsForBouncerManager.getShouldHideStatusBarIconsForBouncer();
}
- private void hideSystemIconArea(boolean animate) {
- animateHide(mSystemIconArea, animate);
+ private void hideEndSideContent(boolean animate) {
+ animateHide(mEndSideContent, animate);
}
- private void showSystemIconArea(boolean animate) {
+ private void showEndSideContent(boolean animate) {
// Only show the system icon area if we are not currently animating
int state = mAnimationScheduler.getAnimationState();
if (state == IDLE || state == SHOWING_PERSISTENT_DOT) {
- animateShow(mSystemIconArea, animate);
+ animateShow(mEndSideContent, animate);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
index 7e9f84c..bebd871 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
@@ -191,6 +191,16 @@
}
@Test
+ public void hideMenuViewWhenStartingAnimation_animatorNotRunning() {
+ mMenuView.show();
+
+ mMenuView.mDragAnimator.start();
+ mMenuView.hide();
+
+ assertThat(mMenuView.mDragAnimator.isRunning()).isFalse();
+ }
+
+ @Test
public void onTargetsChanged_singleTarget_expectedRadii() {
final Position alignRightPosition = new Position(1.0f, 0.0f);
final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/AirQualityColorPickerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/AirQualityColorPickerTest.java
deleted file mode 100644
index 33be5dc..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/AirQualityColorPickerTest.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.dreams.complication;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.testing.AndroidTestingRunner;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class AirQualityColorPickerTest extends SysuiTestCase {
- private static final int DEFAULT_COLOR = 0;
- private static final int MOCK_COLOR_1 = 1;
- private static final int MOCK_COLOR_2 = 2;
- private static final int MOCK_COLOR_3 = 3;
- private static final int MOCK_COLOR_4 = 4;
- private static final int MOCK_COLOR_5 = 5;
-
- private static final int[] MOCK_THRESHOLDS = {-1, 100, 200, 201, 500};
- private static final int[] MOCK_COLORS =
- {MOCK_COLOR_1, MOCK_COLOR_2, MOCK_COLOR_3, MOCK_COLOR_4, MOCK_COLOR_5};
- private static final int[] EMPTY_ARRAY = {};
-
- @Test
- public void testEmptyThresholds() {
- final AirQualityColorPicker colorPicker = new AirQualityColorPicker(
- EMPTY_ARRAY,
- MOCK_COLORS,
- DEFAULT_COLOR);
- assertThat(colorPicker.getColorForValue("110 AQI")).isEqualTo(DEFAULT_COLOR);
- }
-
- @Test
- public void testEmptyColors() {
- final AirQualityColorPicker colorPicker = new AirQualityColorPicker(
- MOCK_THRESHOLDS,
- EMPTY_ARRAY,
- DEFAULT_COLOR);
- assertThat(colorPicker.getColorForValue("110 AQI")).isEqualTo(DEFAULT_COLOR);
- }
-
- @Test
- public void testEmptyAqiString() {
- final AirQualityColorPicker colorPicker = new AirQualityColorPicker(
- MOCK_THRESHOLDS,
- MOCK_COLORS,
- DEFAULT_COLOR);
- assertThat(colorPicker.getColorForValue("")).isEqualTo(DEFAULT_COLOR);
- }
-
- @Test
- public void testInvalidAqiString() {
- final AirQualityColorPicker colorPicker = new AirQualityColorPicker(
- MOCK_THRESHOLDS,
- MOCK_COLORS,
- DEFAULT_COLOR);
- assertThat(colorPicker.getColorForValue("invalid")).isEqualTo(DEFAULT_COLOR);
- }
-
- @Test
- public void testZeroAirQuality() {
- final AirQualityColorPicker colorPicker = new AirQualityColorPicker(
- MOCK_THRESHOLDS,
- MOCK_COLORS,
- DEFAULT_COLOR);
- assertThat(colorPicker.getColorForValue("0 AQI")).isEqualTo(MOCK_COLOR_1);
- }
-
- @Test
- public void testVeryLargeAirQuality() {
- final AirQualityColorPicker colorPicker = new AirQualityColorPicker(
- MOCK_THRESHOLDS,
- MOCK_COLORS,
- DEFAULT_COLOR);
- assertThat(colorPicker.getColorForValue("100000 AQI")).isEqualTo(MOCK_COLOR_5);
- }
-
- @Test
- public void testAirQuality200() {
- final AirQualityColorPicker colorPicker = new AirQualityColorPicker(
- MOCK_THRESHOLDS,
- MOCK_COLORS,
- DEFAULT_COLOR);
- assertThat(colorPicker.getColorForValue("200 AQI")).isEqualTo(MOCK_COLOR_2);
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamAirQualityComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamAirQualityComplicationTest.java
deleted file mode 100644
index b8a7059..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamAirQualityComplicationTest.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.dreams.complication;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import android.testing.AndroidTestingRunner;
-import android.widget.TextView;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dreams.DreamOverlayStateController;
-import com.android.systemui.dreams.complication.DreamAirQualityComplication.DreamAirQualityViewController;
-import com.android.systemui.dreams.smartspace.DreamSmartspaceController;
-import com.android.systemui.plugins.ActivityStarter;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class DreamAirQualityComplicationTest extends SysuiTestCase {
- private static final String TRAMPOLINE_COMPONENT = "TestComponent";
-
- @Mock
- private DreamSmartspaceController mDreamSmartspaceController;
-
- @Mock
- private DreamOverlayStateController mDreamOverlayStateController;
-
- @Mock
- private DreamAirQualityComplication mComplication;
-
- @Mock
- private AirQualityColorPicker mColorPicker;
-
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- }
-
- /**
- * Ensures {@link DreamAirQualityComplication} is registered.
- */
- @Test
- public void testComplicationRegistered() {
- final DreamAirQualityComplication.Registrant registrant =
- new DreamAirQualityComplication.Registrant(
- mContext,
- mDreamOverlayStateController,
- mComplication);
- registrant.start();
- verify(mDreamOverlayStateController).addComplication(eq(mComplication));
- }
-
- @Test
- public void testGetUnfilteredTargets() {
- final DreamAirQualityViewController controller =
- new DreamAirQualityViewController(
- mock(TextView.class),
- mDreamSmartspaceController,
- TRAMPOLINE_COMPONENT,
- mock(ActivityStarter.class),
- mColorPicker);
- controller.onViewAttached();
- verify(mDreamSmartspaceController).addUnfilteredListener(any());
- controller.onViewDetached();
- verify(mDreamSmartspaceController).removeUnfilteredListener(any());
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamWeatherComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamWeatherComplicationTest.java
deleted file mode 100644
index a23c4b5..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamWeatherComplicationTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.dreams.complication;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.testing.AndroidTestingRunner;
-import android.widget.TextView;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dreams.DreamOverlayStateController;
-import com.android.systemui.dreams.smartspace.DreamSmartspaceController;
-import com.android.systemui.plugins.ActivityStarter;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class DreamWeatherComplicationTest extends SysuiTestCase {
- private static final String TRAMPOLINE_COMPONENT = "TestComponent";
-
- @SuppressWarnings("HidingField")
- @Mock
- private Context mContext;
-
- @Mock
- private DreamSmartspaceController mDreamSmartspaceController;
-
- @Mock
- private DreamOverlayStateController mDreamOverlayStateController;
-
- @Mock
- private DreamWeatherComplication mComplication;
-
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- }
-
- /**
- * Ensures {@link DreamWeatherComplication} is registered.
- */
- @Test
- public void testComplicationRegistered() {
- final DreamWeatherComplication.Registrant registrant =
- new DreamWeatherComplication.Registrant(
- mContext,
- mDreamOverlayStateController,
- mComplication);
- registrant.start();
- verify(mDreamOverlayStateController).addComplication(eq(mComplication));
- }
-
- @Test
- public void testGetUnfilteredTargets() {
- final DreamWeatherComplication.DreamWeatherViewController controller =
- new DreamWeatherComplication.DreamWeatherViewController(mock(
- TextView.class), TRAMPOLINE_COMPONENT, mock(ActivityStarter.class),
- mDreamSmartspaceController, mock(Resources.class));
- controller.onViewAttached();
- verify(mDreamSmartspaceController).addUnfilteredListener(any());
- controller.onViewDetached();
- verify(mDreamSmartspaceController).removeUnfilteredListener(any());
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java
index 99f21ad..c8ebd12 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java
@@ -48,6 +48,8 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
import com.android.systemui.wmshell.BubblesManager;
import com.android.wm.shell.bubbles.Bubble;
@@ -106,6 +108,9 @@
private Intent mIntent;
+ private FakeSystemClock mFakeSystemClock = new FakeSystemClock();
+ private FakeExecutor mBgExecutor = new FakeExecutor(mFakeSystemClock);
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -114,7 +119,8 @@
mNotifCollection,
Optional.of(mBubblesManager),
mUserManager,
- mCommandQueue
+ mCommandQueue,
+ mBgExecutor
);
verify(mCommandQueue, times(1)).addCallback(mCallbacksCaptor.capture());
@@ -193,6 +199,7 @@
// Ensure callback removed
verify(mCommandQueue).removeCallback(any());
// Clear the notification for bubbles.
+ FakeExecutor.exhaustExecutors(mBgExecutor);
verify(mIStatusBarService, times(1)).onNotificationClear(any(),
anyInt(), any(), anyInt(), anyInt(), mNotificationVisibilityCaptor.capture());
// Do not select the bubble.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
index d6faaee..b6f8326 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
@@ -7,6 +7,8 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.qs.QS
import com.android.systemui.shade.NotificationPanelViewController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
@@ -19,6 +21,7 @@
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@@ -32,6 +35,7 @@
@Mock private lateinit var splitShadeOverScroller: SplitShadeOverScroller
@Mock private lateinit var scrimShadeTransitionController: ScrimShadeTransitionController
@Mock private lateinit var dumpManager: DumpManager
+ @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
private lateinit var controller: ShadeTransitionController
@@ -50,7 +54,9 @@
context,
splitShadeOverScrollerFactory = { _, _ -> splitShadeOverScroller },
noOpOverScroller,
- scrimShadeTransitionController)
+ scrimShadeTransitionController,
+ statusBarStateController,
+ )
// Resetting as they are notified upon initialization.
reset(noOpOverScroller, splitShadeOverScroller)
@@ -80,6 +86,45 @@
}
@Test
+ fun onPanelStateChanged_inSplitShade_onKeyguard_forwardsToNoOpOverScroller() {
+ initLateProperties()
+ enableSplitShade()
+ setOnKeyguard()
+
+ startPanelExpansion()
+
+ verify(noOpOverScroller).onPanelStateChanged(STATE_OPENING)
+ verify(noOpOverScroller).onDragDownAmountChanged(DEFAULT_DRAG_DOWN_AMOUNT)
+ verifyZeroInteractions(splitShadeOverScroller)
+ }
+
+ @Test
+ fun onPanelStateChanged_inSplitShade_onLockedShade_forwardsToNoOpOverScroller() {
+ initLateProperties()
+ enableSplitShade()
+ setOnLockedShade()
+
+ startPanelExpansion()
+
+ verify(noOpOverScroller).onPanelStateChanged(STATE_OPENING)
+ verify(noOpOverScroller).onDragDownAmountChanged(DEFAULT_DRAG_DOWN_AMOUNT)
+ verifyZeroInteractions(splitShadeOverScroller)
+ }
+
+ @Test
+ fun onPanelExpansionChanged_inSplitShade_onUnlockedShade_forwardsToSplitShadeOverScroller() {
+ initLateProperties()
+ enableSplitShade()
+ setOnUnlockedShade()
+
+ startPanelExpansion()
+
+ verify(splitShadeOverScroller).onPanelStateChanged(STATE_OPENING)
+ verify(splitShadeOverScroller).onDragDownAmountChanged(DEFAULT_DRAG_DOWN_AMOUNT)
+ verifyZeroInteractions(noOpOverScroller)
+ }
+
+ @Test
fun onPanelStateChanged_notInSplitShade_forwardsToNoOpOverScroller() {
initLateProperties()
disableSplitShade()
@@ -129,6 +174,23 @@
)
}
+ private fun setOnKeyguard() {
+ setShadeState(StatusBarState.KEYGUARD)
+ }
+
+ private fun setOnLockedShade() {
+ setShadeState(StatusBarState.SHADE_LOCKED)
+ }
+
+ private fun setOnUnlockedShade() {
+ setShadeState(StatusBarState.SHADE)
+ }
+
+ private fun setShadeState(state: Int) {
+ whenever(statusBarStateController.state).thenReturn(state)
+ whenever(statusBarStateController.currentOrUpcomingState).thenReturn(state)
+ }
+
companion object {
private const val DEFAULT_DRAG_DOWN_AMOUNT = 123f
private val DEFAULT_EXPANSION_EVENT =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconListTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconListTest.java
deleted file mode 100644
index 4c20b61..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarIconListTest.java
+++ /dev/null
@@ -1,175 +0,0 @@
-package com.android.systemui.statusbar;
-
-import static com.android.systemui.statusbar.phone.StatusBarIconController.TAG_PRIMARY;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.test.suitebuilder.annotation.SmallTest;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.phone.StatusBarIconHolder;
-import com.android.systemui.statusbar.phone.StatusBarIconList;
-import com.android.systemui.statusbar.phone.StatusBarIconList.Slot;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class StatusBarIconListTest extends SysuiTestCase {
-
- private final static String[] STATUS_BAR_SLOTS = {"aaa", "bbb", "ccc"};
-
- @Test
- public void testGetExistingSlot() {
- StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
- assertEquals(1, statusBarIconList.getSlotIndex("bbb"));
- assertEquals(2, statusBarIconList.getSlotIndex("ccc"));
- }
-
- @Test
- public void testGetNonexistingSlot() {
- StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
- assertEquals(0, statusBarIconList.getSlotIndex("aaa"));
- assertEquals(3, statusBarIconList.size());
- assertEquals(0, statusBarIconList.getSlotIndex("zzz")); // new content added in front
- assertEquals(1, statusBarIconList.getSlotIndex("aaa")); // slid back
- assertEquals(4, statusBarIconList.size());
- }
-
- @Test
- public void testAddSlotSlidesIcons() {
- StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
- StatusBarIconHolder sbHolder = mock(StatusBarIconHolder.class);
- statusBarIconList.setIcon(0, sbHolder);
- statusBarIconList.getSlotIndex("zzz"); // new content added in front
- assertNull(statusBarIconList.getIcon(0, TAG_PRIMARY));
- assertEquals(sbHolder, statusBarIconList.getIcon(1, TAG_PRIMARY));
- }
-
- @Test
- public void testGetAndSetIcon() {
- StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
- StatusBarIconHolder sbHolderA = mock(StatusBarIconHolder.class);
- StatusBarIconHolder sbHolderB = mock(StatusBarIconHolder.class);
- statusBarIconList.setIcon(0, sbHolderA);
- statusBarIconList.setIcon(1, sbHolderB);
- assertEquals(sbHolderA, statusBarIconList.getIcon(0, TAG_PRIMARY));
- assertEquals(sbHolderB, statusBarIconList.getIcon(1, TAG_PRIMARY));
- assertNull(statusBarIconList.getIcon(2, TAG_PRIMARY)); // icon not set
- }
-
- @Test
- public void testRemoveIcon() {
- StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
- StatusBarIconHolder sbHolderA = mock(StatusBarIconHolder.class);
- StatusBarIconHolder sbHolderB = mock(StatusBarIconHolder.class);
- statusBarIconList.setIcon(0, sbHolderA);
- statusBarIconList.setIcon(1, sbHolderB);
- statusBarIconList.removeIcon(0, TAG_PRIMARY);
- assertNull(statusBarIconList.getIcon(0, TAG_PRIMARY)); // icon not set
- }
-
- @Test
- public void testGetViewIndex_NoMultiples() {
- StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
- StatusBarIconHolder sbHolder = mock(StatusBarIconHolder.class);
- statusBarIconList.setIcon(2, sbHolder);
- // Icon for item 2 is 0th child view.
- assertEquals(0, statusBarIconList.getViewIndex(2, TAG_PRIMARY));
- statusBarIconList.setIcon(0, sbHolder);
- // Icon for item 0 is 0th child view,
- assertEquals(0, statusBarIconList.getViewIndex(0, TAG_PRIMARY));
- // and item 2 is now 1st child view.
- assertEquals(1, statusBarIconList.getViewIndex(2, TAG_PRIMARY));
- }
-
- @Test
- public void testGetViewIndex_MultipleIconsPerSlot() {
- StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
- StatusBarIconHolder sbHolder = mock(StatusBarIconHolder.class);
-
- statusBarIconList.setIcon(2, sbHolder); // item 2, one icon 0th child
-
- // All of these can be added to the same slot
- // no tag bc it defaults to 0
- StatusBarIconHolder sbHolder2 = mock(StatusBarIconHolder.class);
- StatusBarIconHolder sbHolder3 = mock(StatusBarIconHolder.class);
- int sb3Tag = 1;
- when(sbHolder3.getTag()).thenReturn(sb3Tag);
- StatusBarIconHolder sbHolder4 = mock(StatusBarIconHolder.class);
- int sb4Tag = 2;
- when(sbHolder4.getTag()).thenReturn(sb4Tag);
-
- // Put a holder at slot 1, verify that it is first
- statusBarIconList.setIcon(1, sbHolder2);
- assertEquals(0, statusBarIconList.getViewIndex(1, TAG_PRIMARY));
-
- // Put another holder at slot 1, verify it's index 0 and the rest come after
- statusBarIconList.setIcon(1, sbHolder3);
- assertEquals(0, statusBarIconList.getViewIndex(1, sb3Tag));
- assertEquals(1, statusBarIconList.getViewIndex(1, TAG_PRIMARY));
- // First icon should be at the end
- assertEquals(2, statusBarIconList.getViewIndex(2, TAG_PRIMARY));
-
- // Put another one in there just for good measure
- statusBarIconList.setIcon(1, sbHolder4);
- assertEquals(0, statusBarIconList.getViewIndex(1, sb4Tag));
- assertEquals(1, statusBarIconList.getViewIndex(1, sb3Tag));
- assertEquals(2, statusBarIconList.getViewIndex(1, TAG_PRIMARY));
- assertEquals(3, statusBarIconList.getViewIndex(2, TAG_PRIMARY));
- }
-
- /**
- * StatusBarIconList.Slot tests
- */
-
- @Test
- public void testSlot_ViewOrder() {
- Slot testSlot = new Slot("test_name", null);
-
- // no tag bc it defaults to 0
- StatusBarIconHolder sbHolder1 = mock(StatusBarIconHolder.class);
- StatusBarIconHolder sbHolder2 = mock(StatusBarIconHolder.class);
- int sb2Tag = 1;
- when(sbHolder2.getTag()).thenReturn(sb2Tag);
- StatusBarIconHolder sbHolder3 = mock(StatusBarIconHolder.class);
- int sb3Tag = 2;
- when(sbHolder3.getTag()).thenReturn(sb3Tag);
-
- // Add 3 icons in the same slot, and verify that the list we get is equal to what we gave
- testSlot.addHolder(sbHolder1);
- testSlot.addHolder(sbHolder2);
- testSlot.addHolder(sbHolder3);
-
- // View order is reverse of the order added
- ArrayList<StatusBarIconHolder> expected = new ArrayList<>();
- expected.add(sbHolder3);
- expected.add(sbHolder2);
- expected.add(sbHolder1);
-
- assertTrue(listsEqual(expected, testSlot.getHolderListInViewOrder()));
- }
-
- private boolean listsEqual(List<StatusBarIconHolder> list1, List<StatusBarIconHolder> list2) {
- if (list1.size() != list2.size()) return false;
-
- for (int i = 0; i < list1.size(); i++) {
- if (!list1.get(i).equals(list2.get(i))) {
- return false;
- }
- }
-
- return true;
- }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
index 16b0376..aeef6b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
@@ -19,6 +19,8 @@
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED;
import static android.service.notification.NotificationListenerService.REASON_CANCEL;
+import static android.service.notification.NotificationStats.DISMISSAL_SHADE;
+import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON;
@@ -41,6 +43,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
@@ -50,6 +53,7 @@
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.os.Handler;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.NotificationListenerService.RankingMap;
@@ -91,7 +95,9 @@
import com.android.systemui.statusbar.notification.row.RowInflaterTask;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.leak.LeakDetector;
+import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.Test;
@@ -138,9 +144,14 @@
@Mock private NotificationMediaManager mNotificationMediaManager;
@Mock private NotificationRowBinder mNotificationRowBinder;
@Mock private NotificationListener mNotificationListener;
+ @Mock private IStatusBarService mStatusBarService;
+
+ private FakeSystemClock mFakeSystemClock = new FakeSystemClock();
+ private FakeExecutor mBgExecutor = new FakeExecutor(mFakeSystemClock);
private int mId;
private NotificationEntry mEntry;
+ private DismissedByUserStats mStats;
private StatusBarNotification mSbn;
private NotificationEntryManager mEntryManager;
@@ -191,6 +202,7 @@
Handler.createAsync(TestableLooper.get(this).getLooper()));
mEntry = createNotification();
+ mStats = defaultStats(mEntry);
mSbn = mEntry.getSbn();
when(mNotifPipelineFlags.isNewPipelineEnabled()).thenReturn(false);
@@ -201,9 +213,10 @@
() -> mNotificationRowBinder,
() -> mRemoteInputManager,
mLeakDetector,
- mock(IStatusBarService.class),
+ mStatusBarService,
NotifLiveDataStoreMocksKt.createNotifLiveDataStoreImplMock(),
- mock(DumpManager.class)
+ mock(DumpManager.class),
+ mBgExecutor
);
mEntryManager.initialize(
mNotificationListener,
@@ -316,6 +329,31 @@
}
@Test
+ public void testPerformRemoveNotification_sendRemovalToServer() throws RemoteException {
+ // GIVEN an entry manager with a notification
+ mEntryManager.addActiveNotificationForTest(mEntry);
+
+ // GIVEN interceptor that doesn't intercept
+ when(mRemoveInterceptor.onNotificationRemoveRequested(
+ eq(mEntry.getKey()), argThat(matchEntryOnKey()), anyInt()))
+ .thenReturn(false);
+
+ // WHEN the notification entry is removed
+ mEntryManager.performRemoveNotification(mSbn, mStats, REASON_CANCEL);
+
+ // THEN notification removal is sent to the server
+ FakeExecutor.exhaustExecutors(mBgExecutor);
+ verify(mStatusBarService).onNotificationClear(
+ mSbn.getPackageName(),
+ mSbn.getUser().getIdentifier(),
+ mSbn.getKey(),
+ mStats.dismissalSurface,
+ mStats.dismissalSentiment,
+ mStats.notificationVisibility);
+ verifyNoMoreInteractions(mStatusBarService);
+ }
+
+ @Test
public void testRemoveNotification_onEntryRemoveNotFiredIfEntryDoesntExist() {
mEntryManager.removeNotification("not_a_real_key", mRankingMap, UNDEFINED_DISMISS_REASON);
@@ -573,23 +611,6 @@
any(NotificationVisibility.class), anyBoolean(), eq(UNDEFINED_DISMISS_REASON));
}
- private NotificationEntry createNotification() {
- Notification.Builder n = new Notification.Builder(mContext, "id")
- .setSmallIcon(R.drawable.ic_person)
- .setContentTitle("Title")
- .setContentText("Text");
-
- return new NotificationEntryBuilder()
- .setPkg(TEST_PACKAGE_NAME)
- .setOpPkg(TEST_PACKAGE_NAME)
- .setUid(TEST_UID)
- .setId(mId++)
- .setNotification(n.build())
- .setChannel(new NotificationChannel("id", "", IMPORTANCE_DEFAULT))
- .setUser(new UserHandle(ActivityManager.getCurrentUser()))
- .build();
- }
-
/* Tests annexed from NotificationDataTest go here */
@Test
@@ -713,4 +734,28 @@
return mManagedNotifs.contains(notificationKey);
}
}
+
+ private NotificationEntry createNotification() {
+ Notification.Builder n = new Notification.Builder(mContext, "id")
+ .setSmallIcon(R.drawable.ic_person)
+ .setContentTitle("Title")
+ .setContentText("Text");
+
+ return new NotificationEntryBuilder()
+ .setPkg(TEST_PACKAGE_NAME)
+ .setOpPkg(TEST_PACKAGE_NAME)
+ .setUid(TEST_UID)
+ .setId(mId++)
+ .setNotification(n.build())
+ .setChannel(new NotificationChannel("id", "", IMPORTANCE_DEFAULT))
+ .setUser(new UserHandle(ActivityManager.getCurrentUser()))
+ .build();
+ }
+
+ private static DismissedByUserStats defaultStats(NotificationEntry entry) {
+ return new DismissedByUserStats(
+ DISMISSAL_SHADE,
+ DISMISS_SENTIMENT_NEUTRAL,
+ NotificationVisibility.obtain(entry.getKey(), 7, 2, true));
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index ef763d9..4df99be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -92,6 +92,7 @@
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
+import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
@@ -146,6 +147,7 @@
private NoManSimulator mNoMan;
private FakeSystemClock mClock = new FakeSystemClock();
+ private FakeExecutor mBgExecutor = new FakeExecutor(mClock);
@Before
public void setUp() {
@@ -162,6 +164,7 @@
mNotifPipelineFlags,
mLogger,
mMainHandler,
+ mBgExecutor,
mEulogizer,
mock(DumpManager.class));
mCollection.attach(mGroupCoalescer);
@@ -461,6 +464,8 @@
DismissedByUserStats stats = defaultStats(entry2);
mCollection.dismissNotification(entry2, defaultStats(entry2));
+ FakeExecutor.exhaustExecutors(mBgExecutor);
+
// THEN we send the dismissal to system server
verify(mStatusBarService).onNotificationClear(
notif2.sbn.getPackageName(),
@@ -674,6 +679,8 @@
mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry,
stats);
+ FakeExecutor.exhaustExecutors(mBgExecutor);
+
// THEN we send the dismissal to system server
verify(mStatusBarService).onNotificationClear(
eq(notif.sbn.getPackageName()),
@@ -1211,6 +1218,7 @@
new Pair<>(entry2, defaultStats(entry2))));
// THEN we send the dismissals to system server
+ FakeExecutor.exhaustExecutors(mBgExecutor);
verify(mStatusBarService).onNotificationClear(
notif1.sbn.getPackageName(),
notif1.sbn.getUser().getIdentifier(),
@@ -1577,6 +1585,7 @@
// WHEN finally dismissing
onDismiss.run();
+ FakeExecutor.exhaustExecutors(mBgExecutor);
verify(mStatusBarService).onNotificationClear(any(), anyInt(), eq(notifEvent.key),
anyInt(), anyInt(), any());
verifyNoMoreInteractions(mStatusBarService);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 54cbe24..72d3c2e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -25,6 +25,7 @@
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking;
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
import static com.google.common.truth.Truth.assertThat;
@@ -32,6 +33,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -96,6 +98,8 @@
NotifPipelineFlags mFlags;
@Mock
KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
+ @Mock
+ PendingIntent mPendingIntent;
private NotificationInterruptStateProviderImpl mNotifInterruptionStateProvider;
@@ -197,7 +201,7 @@
ensureStateForHeadsUpWhenAwake();
// WHEN this entry should be filtered out
- NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
+ NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
when(mNotificationFilter.shouldFilterOut(entry)).thenReturn(true);
// THEN we shouldn't heads up this entry
@@ -207,7 +211,7 @@
@Test
public void testDoNotRunFilterOnNewPipeline() {
// WHEN this entry should be filtered out
- NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
+ NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
mNotifInterruptionStateProvider.shouldHeadsUp(entry);
verify(mNotificationFilter, times(0)).shouldFilterOut(eq(entry));
}
@@ -422,6 +426,122 @@
assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
}
+ @Test
+ public void testShouldNotFullScreen_notPendingIntent() throws RemoteException {
+ NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
+ when(mPowerManager.isInteractive()).thenReturn(true);
+ when(mDreamManager.isDreaming()).thenReturn(false);
+ when(mStatusBarStateController.getState()).thenReturn(SHADE);
+
+ assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
+ .isFalse();
+ verify(mLogger, never()).logNoFullscreen(any(), any());
+ verify(mLogger, never()).logNoFullscreenWarning(any(), any());
+ verify(mLogger, never()).logFullscreen(any(), any());
+ }
+
+ @Test
+ public void testShouldNotFullScreen_notHighImportance() throws RemoteException {
+ NotificationEntry entry = createFsiNotification(IMPORTANCE_DEFAULT, /* silenced */ false);
+ when(mPowerManager.isInteractive()).thenReturn(true);
+ when(mDreamManager.isDreaming()).thenReturn(false);
+ when(mStatusBarStateController.getState()).thenReturn(SHADE);
+
+ assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
+ .isFalse();
+ verify(mLogger).logNoFullscreen(entry, "Not important enough");
+ verify(mLogger, never()).logNoFullscreenWarning(any(), any());
+ verify(mLogger, never()).logFullscreen(any(), any());
+ }
+
+ @Test
+ public void testShouldNotFullScreen_isGroupAlertSilenced() throws RemoteException {
+ NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ true);
+ when(mPowerManager.isInteractive()).thenReturn(false);
+ when(mDreamManager.isDreaming()).thenReturn(true);
+ when(mStatusBarStateController.getState()).thenReturn(KEYGUARD);
+
+ assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
+ .isFalse();
+ verify(mLogger, never()).logNoFullscreen(any(), any());
+ verify(mLogger).logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN");
+ verify(mLogger, never()).logFullscreen(any(), any());
+ }
+
+ @Test
+ public void testShouldFullScreen_notInteractive() throws RemoteException {
+ NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
+ when(mPowerManager.isInteractive()).thenReturn(false);
+ when(mDreamManager.isDreaming()).thenReturn(false);
+ when(mStatusBarStateController.getState()).thenReturn(SHADE);
+
+ assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
+ .isTrue();
+ verify(mLogger, never()).logNoFullscreen(any(), any());
+ verify(mLogger, never()).logNoFullscreenWarning(any(), any());
+ verify(mLogger).logFullscreen(entry, "Device is not interactive");
+ }
+
+ @Test
+ public void testShouldFullScreen_isDreaming() throws RemoteException {
+ NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
+ when(mPowerManager.isInteractive()).thenReturn(true);
+ when(mDreamManager.isDreaming()).thenReturn(true);
+ when(mStatusBarStateController.getState()).thenReturn(SHADE);
+
+ assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
+ .isTrue();
+ verify(mLogger, never()).logNoFullscreen(any(), any());
+ verify(mLogger, never()).logNoFullscreenWarning(any(), any());
+ verify(mLogger).logFullscreen(entry, "Device is dreaming");
+ }
+
+ @Test
+ public void testShouldFullScreen_onKeyguard() throws RemoteException {
+ NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
+ when(mPowerManager.isInteractive()).thenReturn(true);
+ when(mDreamManager.isDreaming()).thenReturn(false);
+ when(mStatusBarStateController.getState()).thenReturn(KEYGUARD);
+
+ assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
+ .isTrue();
+ verify(mLogger, never()).logNoFullscreen(any(), any());
+ verify(mLogger, never()).logNoFullscreenWarning(any(), any());
+ verify(mLogger).logFullscreen(entry, "Keyguard is showing");
+ }
+
+ @Test
+ public void testShouldNotFullScreen_willHun() throws RemoteException {
+ NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
+ when(mPowerManager.isInteractive()).thenReturn(true);
+ when(mPowerManager.isScreenOn()).thenReturn(true);
+ when(mDreamManager.isDreaming()).thenReturn(false);
+ when(mStatusBarStateController.getState()).thenReturn(SHADE);
+
+ assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
+ .isFalse();
+ verify(mLogger).logNoFullscreen(entry, "Expected to HUN");
+ verify(mLogger, never()).logNoFullscreenWarning(any(), any());
+ verify(mLogger, never()).logFullscreen(any(), any());
+ }
+
+ @Test
+ public void testShouldFullScreen_packageSnoozed() throws RemoteException {
+ NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
+ when(mPowerManager.isInteractive()).thenReturn(true);
+ when(mPowerManager.isScreenOn()).thenReturn(true);
+ when(mDreamManager.isDreaming()).thenReturn(false);
+ when(mStatusBarStateController.getState()).thenReturn(SHADE);
+ when(mHeadsUpManager.isSnoozed("a")).thenReturn(true);
+
+ assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
+ .isTrue();
+ verify(mLogger).logNoHeadsUpPackageSnoozed(entry);
+ verify(mLogger, never()).logNoFullscreen(any(), any());
+ verify(mLogger, never()).logNoFullscreenWarning(any(), any());
+ verify(mLogger).logFullscreen(entry, "Expected not to HUN");
+ }
+
/**
* Bubbles can happen.
*/
@@ -516,8 +636,8 @@
private NotificationEntry createBubble(String groupKey, Integer groupAlert) {
Notification.BubbleMetadata data = new Notification.BubbleMetadata.Builder(
PendingIntent.getActivity(mContext, 0, new Intent(),
- PendingIntent.FLAG_MUTABLE),
- Icon.createWithResource(mContext.getResources(), R.drawable.android))
+ PendingIntent.FLAG_MUTABLE),
+ Icon.createWithResource(mContext.getResources(), R.drawable.android))
.build();
Notification.Builder nb = new Notification.Builder(getContext(), "a")
.setContentTitle("title")
@@ -549,6 +669,10 @@
.setContentText("content text")
.build();
+ return createNotification(importance, n);
+ }
+
+ private NotificationEntry createNotification(int importance, Notification n) {
return new NotificationEntryBuilder()
.setPkg("a")
.setOpPkg("a")
@@ -559,45 +683,57 @@
.build();
}
+ private NotificationEntry createFsiNotification(int importance, boolean silent) {
+ Notification n = new Notification.Builder(getContext(), "a")
+ .setContentTitle("title")
+ .setContentText("content text")
+ .setFullScreenIntent(mPendingIntent, true)
+ .setGroup("fsi")
+ .setGroupAlertBehavior(silent ? GROUP_ALERT_SUMMARY : Notification.GROUP_ALERT_ALL)
+ .build();
+
+ return createNotification(importance, n);
+ }
+
private final NotificationInterruptSuppressor
mSuppressAwakeHeadsUp =
new NotificationInterruptSuppressor() {
- @Override
- public String getName() {
- return "suppressAwakeHeadsUp";
- }
+ @Override
+ public String getName() {
+ return "suppressAwakeHeadsUp";
+ }
- @Override
- public boolean suppressAwakeHeadsUp(NotificationEntry entry) {
- return true;
- }
- };
+ @Override
+ public boolean suppressAwakeHeadsUp(NotificationEntry entry) {
+ return true;
+ }
+ };
private final NotificationInterruptSuppressor
mSuppressAwakeInterruptions =
new NotificationInterruptSuppressor() {
- @Override
- public String getName() {
- return "suppressAwakeInterruptions";
- }
+ @Override
+ public String getName() {
+ return "suppressAwakeInterruptions";
+ }
- @Override
- public boolean suppressAwakeInterruptions(NotificationEntry entry) {
- return true;
- }
- };
+ @Override
+ public boolean suppressAwakeInterruptions(NotificationEntry entry) {
+ return true;
+ }
+ };
private final NotificationInterruptSuppressor
mSuppressInterruptions =
new NotificationInterruptSuppressor() {
- @Override
- public String getName() {
- return "suppressInterruptions";
- }
+ @Override
+ public String getName() {
+ return "suppressInterruptions";
+ }
- @Override
- public boolean suppressInterruptions(NotificationEntry entry) {
- return true;
- }
- };
+ @Override
+ public boolean suppressInterruptions(NotificationEntry entry) {
+ return true;
+ }
+ };
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
index 251ac7d..bf7549a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
@@ -194,7 +194,8 @@
mLeakDetector,
mock(IStatusBarService.class),
NotifLiveDataStoreMocksKt.createNotifLiveDataStoreImplMock(),
- mock(DumpManager.class)
+ mock(DumpManager.class),
+ mBgExecutor
);
mEntryManager.initialize(
mNotificationListener,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconListTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconListTest.java
new file mode 100644
index 0000000..f0a4f3f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconListTest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import static com.android.systemui.statusbar.phone.StatusBarIconController.TAG_PRIMARY;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.phone.StatusBarIconList.Slot;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class StatusBarIconListTest extends SysuiTestCase {
+
+ private final static String[] STATUS_BAR_SLOTS = {"aaa", "bbb", "ccc"};
+
+ @Test
+ public void testGetExistingSlot() {
+ StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
+
+ List<Slot> slots = statusBarIconList.getSlots();
+ assertEquals(3, slots.size());
+ assertEquals("aaa", slots.get(0).getName());
+ assertEquals("bbb", slots.get(1).getName());
+ assertEquals("ccc", slots.get(2).getName());
+ }
+
+ @Test
+ public void testGetNonexistingSlot() {
+ StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
+
+ statusBarIconList.getSlot("zzz");
+
+ List<Slot> slots = statusBarIconList.getSlots();
+ assertEquals(4, slots.size());
+ // new content added in front, so zzz should be first and aaa should slide back to second
+ assertEquals("zzz", slots.get(0).getName());
+ assertEquals("aaa", slots.get(1).getName());
+ }
+
+ @Test
+ public void testAddSlotSlidesIcons() {
+ StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
+ StatusBarIconHolder sbHolder = mock(StatusBarIconHolder.class);
+ statusBarIconList.setIcon("aaa", sbHolder);
+
+ statusBarIconList.getSlot("zzz");
+
+ List<Slot> slots = statusBarIconList.getSlots();
+ // new content added in front, so the holder we set on "aaa" should show up at index 1
+ assertNull(slots.get(0).getHolderForTag(TAG_PRIMARY));
+ assertEquals(sbHolder, slots.get(1).getHolderForTag(TAG_PRIMARY));
+ }
+
+ @Test
+ public void testGetAndSetIcon() {
+ StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
+ StatusBarIconHolder sbHolderA = mock(StatusBarIconHolder.class);
+ StatusBarIconHolder sbHolderB = mock(StatusBarIconHolder.class);
+
+ statusBarIconList.setIcon("aaa", sbHolderA);
+ statusBarIconList.setIcon("bbb", sbHolderB);
+
+ assertEquals(sbHolderA, statusBarIconList.getIconHolder("aaa", TAG_PRIMARY));
+ assertEquals(sbHolderB, statusBarIconList.getIconHolder("bbb", TAG_PRIMARY));
+ assertNull(statusBarIconList.getIconHolder("ccc", TAG_PRIMARY)); // icon not set
+ }
+
+ @Test
+ public void testRemoveIcon() {
+ StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
+ StatusBarIconHolder sbHolderA = mock(StatusBarIconHolder.class);
+ StatusBarIconHolder sbHolderB = mock(StatusBarIconHolder.class);
+
+ statusBarIconList.setIcon("aaa", sbHolderA);
+ statusBarIconList.setIcon("bbb", sbHolderB);
+
+ statusBarIconList.removeIcon("aaa", TAG_PRIMARY);
+
+ assertNull(statusBarIconList.getIconHolder("aaa", TAG_PRIMARY)); // icon not set
+ }
+
+ @Test
+ public void testGetViewIndex_NoMultiples() {
+ StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
+ StatusBarIconHolder sbHolder = mock(StatusBarIconHolder.class);
+
+ statusBarIconList.setIcon("ccc", sbHolder);
+
+ // Since only "ccc" has a holder set, it should be first
+ assertEquals(0, statusBarIconList.getViewIndex("ccc", TAG_PRIMARY));
+
+ // Now, also set a holder for "aaa"
+ statusBarIconList.setIcon("aaa", sbHolder);
+
+ // Then "aaa" gets the first view index and "ccc" gets the second
+ assertEquals(0, statusBarIconList.getViewIndex("aaa", TAG_PRIMARY));
+ assertEquals(1, statusBarIconList.getViewIndex("ccc", TAG_PRIMARY));
+ }
+
+ @Test
+ public void testGetViewIndex_MultipleIconsPerSlot() {
+ StatusBarIconList statusBarIconList = new StatusBarIconList(STATUS_BAR_SLOTS);
+ StatusBarIconHolder sbHolder = mock(StatusBarIconHolder.class);
+
+ statusBarIconList.setIcon("ccc", sbHolder);
+
+ // All of these can be added to the same slot
+ // no tag bc it defaults to 0
+ StatusBarIconHolder sbHolder2 = mock(StatusBarIconHolder.class);
+ StatusBarIconHolder sbHolder3 = mock(StatusBarIconHolder.class);
+ int sb3Tag = 1;
+ when(sbHolder3.getTag()).thenReturn(sb3Tag);
+ StatusBarIconHolder sbHolder4 = mock(StatusBarIconHolder.class);
+ int sb4Tag = 2;
+ when(sbHolder4.getTag()).thenReturn(sb4Tag);
+
+ // Put a holder for "bbb", verify that it is first
+ statusBarIconList.setIcon("bbb", sbHolder2);
+ assertEquals(0, statusBarIconList.getViewIndex("bbb", TAG_PRIMARY));
+
+ // Put another holder for "bbb" at slot 1, verify its index 0 and the rest come after
+ statusBarIconList.setIcon("bbb", sbHolder3);
+ assertEquals(0, statusBarIconList.getViewIndex("bbb", sb3Tag));
+ assertEquals(1, statusBarIconList.getViewIndex("bbb", TAG_PRIMARY));
+ // "ccc" should appear at the end
+ assertEquals(2, statusBarIconList.getViewIndex("ccc", TAG_PRIMARY));
+
+ // Put another one in "bbb" just for good measure
+ statusBarIconList.setIcon("bbb", sbHolder4);
+ assertEquals(0, statusBarIconList.getViewIndex("bbb", sb4Tag));
+ assertEquals(1, statusBarIconList.getViewIndex("bbb", sb3Tag));
+ assertEquals(2, statusBarIconList.getViewIndex("bbb", TAG_PRIMARY));
+ assertEquals(3, statusBarIconList.getViewIndex("ccc", TAG_PRIMARY));
+ }
+
+ /**
+ * StatusBarIconList.Slot tests
+ */
+
+ @Test
+ public void testSlot_ViewOrder() {
+ Slot testSlot = new Slot("test_name", null);
+
+ // no tag bc it defaults to 0
+ StatusBarIconHolder sbHolder1 = mock(StatusBarIconHolder.class);
+ StatusBarIconHolder sbHolder2 = mock(StatusBarIconHolder.class);
+ int sb2Tag = 1;
+ when(sbHolder2.getTag()).thenReturn(sb2Tag);
+ StatusBarIconHolder sbHolder3 = mock(StatusBarIconHolder.class);
+ int sb3Tag = 2;
+ when(sbHolder3.getTag()).thenReturn(sb3Tag);
+
+ // Add 3 icons in the same slot, and verify that the list we get is equal to what we gave
+ testSlot.addHolder(sbHolder1);
+ testSlot.addHolder(sbHolder2);
+ testSlot.addHolder(sbHolder3);
+
+ // View order is reverse of the order added
+ ArrayList<StatusBarIconHolder> expected = new ArrayList<>();
+ expected.add(sbHolder3);
+ expected.add(sbHolder2);
+ expected.add(sbHolder1);
+
+ assertTrue(listsEqual(expected, testSlot.getHolderListInViewOrder()));
+ }
+
+ private boolean listsEqual(List<StatusBarIconHolder> list1, List<StatusBarIconHolder> list2) {
+ if (list1.size() != list2.size()) return false;
+
+ for (int i = 0; i < list1.size(); i++) {
+ if (!list1.get(i).equals(list2.get(i))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 3f14494..681e998 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -124,7 +124,7 @@
fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
- assertEquals(View.VISIBLE, getSystemIconAreaView().getVisibility());
+ assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
assertEquals(View.VISIBLE, getClockView().getVisibility());
}
@@ -134,11 +134,11 @@
fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_SYSTEM_INFO, 0, false);
- assertEquals(View.INVISIBLE, getSystemIconAreaView().getVisibility());
+ assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
- assertEquals(View.VISIBLE, getSystemIconAreaView().getVisibility());
+ assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
}
@Test
@@ -234,7 +234,7 @@
fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
- assertEquals(View.VISIBLE, getSystemIconAreaView().getVisibility());
+ assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
assertEquals(View.VISIBLE, getClockView().getVisibility());
}
@@ -246,7 +246,7 @@
fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
- assertEquals(View.VISIBLE, getSystemIconAreaView().getVisibility());
+ assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
assertEquals(View.VISIBLE, getClockView().getVisibility());
}
@@ -257,12 +257,12 @@
when(mNotificationPanelViewController.hasCustomClock()).thenReturn(true);
// Make sure they start out as visible
- assertEquals(View.VISIBLE, getSystemIconAreaView().getVisibility());
+ assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
assertEquals(View.VISIBLE, getClockView().getVisibility());
fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
- assertEquals(View.INVISIBLE, getSystemIconAreaView().getVisibility());
+ assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
assertEquals(View.GONE, getClockView().getVisibility());
}
@@ -273,14 +273,14 @@
when(mNotificationPanelViewController.hasCustomClock()).thenReturn(true);
// Make sure they start out as visible
- assertEquals(View.VISIBLE, getSystemIconAreaView().getVisibility());
+ assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
assertEquals(View.VISIBLE, getClockView().getVisibility());
fragment.onDozingChanged(true);
// When this callback is triggered, we want to make sure the clock and system info
// visibilities are recalculated. Since dozing=true, they shouldn't be visible.
- assertEquals(View.INVISIBLE, getSystemIconAreaView().getVisibility());
+ assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
assertEquals(View.GONE, getClockView().getVisibility());
}
@@ -419,7 +419,7 @@
return mFragment.getView().findViewById(R.id.clock);
}
- private View getSystemIconAreaView() {
- return mFragment.getView().findViewById(R.id.system_icon_area);
+ private View getEndSideContentView() {
+ return mFragment.getView().findViewById(R.id.status_bar_end_side_content);
}
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 00d728b..07a5fb5 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -14389,6 +14389,19 @@
final int callingPid = Binder.getCallingPid();
final int callingUid = Binder.getCallingUid();
+ // Non-system callers can't declare that a broadcast is alarm-related.
+ // The PendingIntent invocation case is handled in PendingIntentRecord.
+ if (bOptions != null && callingUid != SYSTEM_UID) {
+ if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) {
+ if (DEBUG_BROADCAST) {
+ Slog.w(TAG, "Non-system caller " + callingUid
+ + " may not flag broadcast as alarm-related");
+ }
+ throw new SecurityException(
+ "Non-system callers may not flag broadcasts as alarm-related");
+ }
+ }
+
final long origId = Binder.clearCallingIdentity();
try {
return broadcastIntentLocked(callerApp,
@@ -14402,6 +14415,7 @@
}
}
+ // Not the binder call surface
int broadcastIntentInPackage(String packageName, @Nullable String featureId, int uid,
int realCallingUid, int realCallingPid, Intent intent, String resolvedType,
IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras,
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 19ffc173..ae91d75 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -70,6 +70,7 @@
final boolean callerInstantApp; // caller is an Instant App?
final boolean ordered; // serialize the send to receivers?
final boolean sticky; // originated from existing sticky data?
+ final boolean alarm; // originated from an alarm triggering?
final boolean initialSticky; // initial broadcast from register to sticky?
final int userId; // user id this broadcast was for
final String resolvedType; // the resolved data type
@@ -305,6 +306,7 @@
this.allowBackgroundActivityStarts = allowBackgroundActivityStarts;
mBackgroundActivityStartsToken = backgroundActivityStartsToken;
this.timeoutExempt = timeoutExempt;
+ alarm = options != null && options.isAlarmBroadcast();
}
/**
@@ -357,6 +359,7 @@
allowBackgroundActivityStarts = from.allowBackgroundActivityStarts;
mBackgroundActivityStartsToken = from.mBackgroundActivityStartsToken;
timeoutExempt = from.timeoutExempt;
+ alarm = from.alarm;
}
/**
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 4044cce..bda60ff 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -18,6 +18,7 @@
import static android.app.ActivityManager.START_SUCCESS;
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST_LIGHT;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -34,6 +35,7 @@
import android.os.IBinder;
import android.os.PowerWhitelistManager;
import android.os.PowerWhitelistManager.ReasonCode;
+import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.TransactionTooLargeException;
@@ -416,6 +418,22 @@
final int callingUid = Binder.getCallingUid();
final int callingPid = Binder.getCallingPid();
+
+ // Only system senders can declare a broadcast to be alarm-originated. We check
+ // this here rather than in the general case handling below to fail before the other
+ // invocation side effects such as allowlisting.
+ if (options != null && callingUid != Process.SYSTEM_UID
+ && key.type == ActivityManager.INTENT_SENDER_BROADCAST) {
+ if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) {
+ if (DEBUG_BROADCAST_LIGHT) {
+ Slog.w(TAG, "Non-system caller " + callingUid
+ + " may not flag broadcast as alarm-related");
+ }
+ throw new SecurityException(
+ "Non-system callers may not flag broadcasts as alarm-related");
+ }
+ }
+
final long origId = Binder.clearCallingIdentity();
int res = START_SUCCESS;
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 90b9967e..d14902e 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -522,10 +522,12 @@
// Set the layer stack.
device.setLayerStackLocked(t, isBlanked ? BLANK_LAYER_STACK : mLayerStack);
// Also inform whether the device is the same one sent to inputflinger for its layerstack.
+ // Prevent displays that are disabled from receiving input.
// TODO(b/188914255): Remove once input can dispatch against device vs layerstack.
device.setDisplayFlagsLocked(t,
- device.getDisplayDeviceInfoLocked().touch != TOUCH_NONE
- ? SurfaceControl.DISPLAY_RECEIVES_INPUT : 0);
+ (isEnabled() && device.getDisplayDeviceInfoLocked().touch != TOUCH_NONE)
+ ? SurfaceControl.DISPLAY_RECEIVES_INPUT
+ : 0);
// Set the color mode and allowed display mode.
if (device == mPrimaryDisplayDevice) {
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 63c5456..7b60345 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -23,6 +23,7 @@
import static com.android.server.wm.ActivityInterceptorCallback.DREAM_MANAGER_ORDERED_ID;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.TaskInfo;
@@ -45,6 +46,9 @@
import android.os.Looper;
import android.os.PowerManager;
import android.os.PowerManagerInternal;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
@@ -221,6 +225,10 @@
}
}
+ protected void requestStartDreamFromShell() {
+ requestDreamInternal();
+ }
+
private void requestDreamInternal() {
// Ask the power manager to nap. It will eventually call back into
// startDream() if/when it is appropriate to start dreaming.
@@ -275,6 +283,10 @@
}
}
+ protected void requestStopDreamFromShell() {
+ stopDreamInternal(true, "stopping dream from shell");
+ }
+
private void stopDreamInternal(boolean immediate, String reason) {
synchronized (mLock) {
stopDreamLocked(immediate, reason);
@@ -593,6 +605,14 @@
}
}
+ public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
+ @Nullable FileDescriptor err,
+ @NonNull String[] args, @Nullable ShellCallback callback,
+ @NonNull ResultReceiver resultReceiver) throws RemoteException {
+ new DreamShellCommand(DreamManagerService.this, mPowerManager)
+ .exec(this, in, out, err, args, callback, resultReceiver);
+ }
+
@Override // Binder call
public ComponentName[] getDreamComponents() {
return getDreamComponentsForUser(UserHandle.getCallingUserId());
diff --git a/services/core/java/com/android/server/dreams/DreamShellCommand.java b/services/core/java/com/android/server/dreams/DreamShellCommand.java
new file mode 100644
index 0000000..eae7e80
--- /dev/null
+++ b/services/core/java/com/android/server/dreams/DreamShellCommand.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.dreams;
+
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import java.io.PrintWriter;
+
+/**
+ * {@link DreamShellCommand} allows accessing dream functionality, including toggling dream state.
+ */
+public class DreamShellCommand extends ShellCommand {
+ private static final boolean DEBUG = true;
+ private static final String TAG = "DreamShellCommand";
+ private final @NonNull DreamManagerService mService;
+ private final @NonNull PowerManager mPowerManager;
+
+ DreamShellCommand(@NonNull DreamManagerService service, @NonNull PowerManager powerManager) {
+ mService = service;
+ mPowerManager = powerManager;
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid != Process.ROOT_UID) {
+ Slog.e(TAG, "Must be root before calling Dream shell commands");
+ return -1;
+ }
+
+ if (TextUtils.isEmpty(cmd)) {
+ return super.handleDefaultCommands(cmd);
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "onCommand:" + cmd);
+ }
+
+ switch (cmd) {
+ case "start-dreaming":
+ return startDreaming();
+ case "stop-dreaming":
+ return stopDreaming();
+ default:
+ return super.handleDefaultCommands(cmd);
+ }
+ }
+
+ private int startDreaming() {
+ mPowerManager.wakeUp(SystemClock.uptimeMillis(),
+ PowerManager.WAKE_REASON_PLUGGED_IN, "shell:cmd:android.service.dreams:DREAM");
+ mService.requestStartDreamFromShell();
+ return 0;
+ }
+
+ private int stopDreaming() {
+ mService.requestStopDreamFromShell();
+ return 0;
+ }
+
+ @Override
+ public void onHelp() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println("Dream manager (dreams) commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println(" start-dreaming");
+ pw.println(" Start the currently configured dream.");
+ pw.println(" stop-dreaming");
+ pw.println(" Stops any active dream");
+ }
+}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index fe66f7a..d9c509c 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -161,6 +161,7 @@
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
+import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ActivityInfo.ScreenOrientation;
@@ -6520,12 +6521,13 @@
/**
* Notifies the remote insets controller that the top focused window has changed.
*
- * @param packageName The name of the package that is open in the top focused window.
+ * @param component The application component that is open in the top focussed window.
* @param requestedVisibilities The insets visibilities requested by the focussed window.
*/
- void topFocusedWindowChanged(String packageName, InsetsVisibilities requestedVisibilities) {
+ void topFocusedWindowChanged(ComponentName component,
+ InsetsVisibilities requestedVisibilities) {
try {
- mRemoteInsetsController.topFocusedWindowChanged(packageName, requestedVisibilities);
+ mRemoteInsetsController.topFocusedWindowChanged(component, requestedVisibilities);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to deliver package in top focused window change", e);
}
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 620a56d..3e2d7c9 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -46,6 +46,7 @@
import android.app.ActivityTaskManager;
import android.app.StatusBarManager;
import android.app.WindowConfiguration;
+import android.content.ComponentName;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.ArrayMap;
@@ -548,8 +549,10 @@
return focusedWin;
}
if (remoteInsetsControllerControlsSystemBars(focusedWin)) {
+ ComponentName component = focusedWin.mActivityRecord != null
+ ? focusedWin.mActivityRecord.mActivityComponent : null;
mDisplayContent.mRemoteInsetsControlTarget.topFocusedWindowChanged(
- focusedWin.mAttrs.packageName, focusedWin.getRequestedVisibilities());
+ component, focusedWin.getRequestedVisibilities());
return mDisplayContent.mRemoteInsetsControlTarget;
}
if (mPolicy.areSystemBarsForcedShownLw()) {
@@ -606,8 +609,10 @@
return null;
}
if (remoteInsetsControllerControlsSystemBars(focusedWin)) {
+ ComponentName component = focusedWin.mActivityRecord != null
+ ? focusedWin.mActivityRecord.mActivityComponent : null;
mDisplayContent.mRemoteInsetsControlTarget.topFocusedWindowChanged(
- focusedWin.mAttrs.packageName, focusedWin.getRequestedVisibilities());
+ component, focusedWin.getRequestedVisibilities());
return mDisplayContent.mRemoteInsetsControlTarget;
}
if (mPolicy.areSystemBarsForcedShownLw()) {
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index f80e732..d09068b 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -600,7 +600,7 @@
}
private SurfaceAnimator startDisplayRotation() {
- return startAnimation(initializeBuilder()
+ SurfaceAnimator animator = startAnimation(initializeBuilder()
.setAnimationLeashParent(mDisplayContent.getSurfaceControl())
.setSurfaceControl(mDisplayContent.getWindowingLayer())
.setParentSurfaceControl(mDisplayContent.getSurfaceControl())
@@ -609,6 +609,13 @@
.build(),
createWindowAnimationSpec(mRotateEnterAnimation),
this::onAnimationEnd);
+
+ // Crop the animation leash to avoid extended wallpaper from showing over
+ // mBackColorSurface
+ Rect displayBounds = mDisplayContent.getBounds();
+ mDisplayContent.getPendingTransaction()
+ .setWindowCrop(animator.mLeash, displayBounds.width(), displayBounds.height());
+ return animator;
}
private SurfaceAnimator startScreenshotAlphaAnimation() {
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index 4942464..67ef7f5 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -722,6 +722,25 @@
}
@Test
+ public void testAlarmBroadcastOption() throws Exception {
+ final long triggerTime = mNowElapsedTest + 5000;
+ final PendingIntent alarmPi = getNewMockPendingIntent();
+ setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, alarmPi);
+
+ mNowElapsedTest = mTestTimer.getElapsed();
+ mTestTimer.expire();
+
+ final ArgumentCaptor<PendingIntent.OnFinished> onFinishedCaptor =
+ ArgumentCaptor.forClass(PendingIntent.OnFinished.class);
+ final ArgumentCaptor<Bundle> optionsCaptor = ArgumentCaptor.forClass(Bundle.class);
+ verify(alarmPi).send(eq(mMockContext), eq(0), any(Intent.class),
+ onFinishedCaptor.capture(), any(Handler.class), isNull(),
+ optionsCaptor.capture());
+ assertTrue(optionsCaptor.getValue()
+ .getBoolean(BroadcastOptions.KEY_ALARM_BROADCAST, false));
+ }
+
+ @Test
public void testUpdateConstants() {
setDeviceConfigLong(KEY_MIN_FUTURITY, 5);
setDeviceConfigLong(KEY_MIN_INTERVAL, 10);
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java
index ece0a627..b0738fd 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayTest.java
@@ -17,7 +17,11 @@
package com.android.server.display;
import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.PropertyInvalidatedCache;
@@ -45,18 +49,21 @@
private LogicalDisplay mLogicalDisplay;
private DisplayDevice mDisplayDevice;
+ private final DisplayDeviceInfo mDisplayDeviceInfo = new DisplayDeviceInfo();
@Before
public void setUp() {
// Share classloader to allow package private access.
System.setProperty("dexmaker.share_classloader", "true");
mDisplayDevice = mock(DisplayDevice.class);
- DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
- displayDeviceInfo.width = DISPLAY_WIDTH;
- displayDeviceInfo.height = DISPLAY_HEIGHT;
- displayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice);
- when(mDisplayDevice.getDisplayDeviceInfoLocked()).thenReturn(displayDeviceInfo);
+
+ mDisplayDeviceInfo.copyFrom(new DisplayDeviceInfo());
+ mDisplayDeviceInfo.width = DISPLAY_WIDTH;
+ mDisplayDeviceInfo.height = DISPLAY_HEIGHT;
+ mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
+ mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_INTERNAL;
+ when(mDisplayDevice.getDisplayDeviceInfoLocked()).thenReturn(mDisplayDeviceInfo);
// Disable binder caches in this process.
PropertyInvalidatedCache.disableForTestMode();
@@ -103,4 +110,33 @@
mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
assertEquals(expectedPosition, mLogicalDisplay.getDisplayPosition());
}
+
+ @Test
+ public void testDisplayInputFlags() {
+ SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+ mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+ verify(t).setDisplayFlags(any(), eq(SurfaceControl.DISPLAY_RECEIVES_INPUT));
+ reset(t);
+
+ mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_NONE;
+ mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+ verify(t).setDisplayFlags(any(), eq(0));
+ reset(t);
+
+ mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_VIRTUAL;
+ mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+ verify(t).setDisplayFlags(any(), eq(SurfaceControl.DISPLAY_RECEIVES_INPUT));
+ reset(t);
+
+ mLogicalDisplay.setPhase(LogicalDisplay.DISPLAY_PHASE_DISABLED);
+ mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+ verify(t).setDisplayFlags(any(), eq(0));
+ reset(t);
+
+ mLogicalDisplay.setPhase(LogicalDisplay.DISPLAY_PHASE_ENABLED);
+ mDisplayDeviceInfo.touch = DisplayDeviceInfo.TOUCH_EXTERNAL;
+ mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
+ verify(t).setDisplayFlags(any(), eq(SurfaceControl.DISPLAY_RECEIVES_INPUT));
+ reset(t);
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 8a539cd..0cbf1b2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -811,7 +811,7 @@
}
@Override
- public void topFocusedWindowChanged(String packageName,
+ public void topFocusedWindowChanged(ComponentName component,
InsetsVisibilities requestedVisibilities) {
}
};
diff --git a/telephony/common/com/android/internal/telephony/SmsApplication.java b/telephony/common/com/android/internal/telephony/SmsApplication.java
index 4924a82..4230225 100644
--- a/telephony/common/com/android/internal/telephony/SmsApplication.java
+++ b/telephony/common/com/android/internal/telephony/SmsApplication.java
@@ -1146,4 +1146,35 @@
}
return null;
}
+
+ /**
+ * Check if a package is default mms app (or equivalent, like bluetooth)
+ *
+ * @param context context from the calling app
+ * @param packageName the name of the package to be checked
+ * @return true if the package is default mms app or bluetooth
+ */
+ @UnsupportedAppUsage
+ public static boolean isDefaultMmsApplication(Context context, String packageName) {
+ if (packageName == null) {
+ return false;
+ }
+ String defaultMmsPackage = getDefaultMmsApplicationPackageName(context);
+ String bluetoothPackageName = context.getResources()
+ .getString(com.android.internal.R.string.config_systemBluetoothStack);
+
+ if ((defaultMmsPackage != null && defaultMmsPackage.equals(packageName))
+ || bluetoothPackageName.equals(packageName)) {
+ return true;
+ }
+ return false;
+ }
+
+ private static String getDefaultMmsApplicationPackageName(Context context) {
+ ComponentName component = getDefaultMmsApplication(context, false);
+ if (component != null) {
+ return component.getPackageName();
+ }
+ return null;
+ }
}
diff --git a/telephony/java/android/telephony/UiccSlotInfo.java b/telephony/java/android/telephony/UiccSlotInfo.java
index 06c5b5c..5e02532 100644
--- a/telephony/java/android/telephony/UiccSlotInfo.java
+++ b/telephony/java/android/telephony/UiccSlotInfo.java
@@ -129,7 +129,7 @@
this.mLogicalSlotIdx = logicalSlotIdx;
this.mIsExtendedApduSupported = isExtendedApduSupported;
this.mIsRemovable = false;
- this.mPortList = null;
+ this.mPortList = new ArrayList<UiccPortInfo>();
}
/**