Merge "Re-introduce event order guaranee in IME client registration"
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 46671b2..9295bb7 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -62,7 +62,6 @@
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
-import android.view.WindowManager.BadTokenException;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CursorAnchorInfo;
@@ -354,7 +353,6 @@
SoftInputWindow mWindow;
boolean mInitialized;
boolean mWindowCreated;
- boolean mWindowAdded;
boolean mWindowVisible;
boolean mWindowWasVisible;
boolean mInShowWindow;
@@ -559,16 +557,7 @@
if (DEBUG) Log.v(TAG, "showSoftInput()");
boolean wasVis = isInputViewShown();
if (dispatchOnShowInputRequested(flags, false)) {
- try {
- showWindow(true);
- } catch (BadTokenException e) {
- // We have ignored BadTokenException here since Jelly Bean MR-2 (API Level 18).
- // We could ignore BadTokenException in InputMethodService#showWindow() instead,
- // but it may break assumptions for those who override #showWindow() that we can
- // detect errors in #showWindow() by checking BadTokenException.
- // TODO: Investigate its feasibility. Update JavaDoc of #showWindow() of
- // whether it's OK to override #showWindow() or not.
- }
+ showWindow(true);
}
clearInsetOfPreviousIme();
// If user uses hard keyboard, IME button should always be shown.
@@ -986,13 +975,7 @@
mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener(
mInsetsComputer);
doFinishInput();
- if (mWindowAdded) {
- // Disable exit animation for the current IME window
- // to avoid the race condition between the exit and enter animations
- // when the current IME is being switched to another one.
- mWindow.getWindow().setWindowAnimations(0);
- mWindow.dismiss();
- }
+ mWindow.dismissForDestroyIfNecessary();
if (mSettingsObserver != null) {
mSettingsObserver.unregister();
mSettingsObserver = null;
@@ -1778,7 +1761,6 @@
public void showWindow(boolean showInput) {
if (DEBUG) Log.v(TAG, "Showing window: showInput=" + showInput
+ " mShowInputRequested=" + mShowInputRequested
- + " mWindowAdded=" + mWindowAdded
+ " mWindowCreated=" + mWindowCreated
+ " mWindowVisible=" + mWindowVisible
+ " mInputStarted=" + mInputStarted
@@ -1788,27 +1770,12 @@
Log.w(TAG, "Re-entrance in to showWindow");
return;
}
-
- try {
- mWindowWasVisible = mWindowVisible;
- mInShowWindow = true;
- showWindowInner(showInput);
- } catch (BadTokenException e) {
- // BadTokenException is a normal consequence in certain situations, e.g., swapping IMEs
- // while there is a DO_SHOW_SOFT_INPUT message in the IIMethodWrapper queue.
- if (DEBUG) Log.v(TAG, "BadTokenException: IME is done.");
- mWindowVisible = false;
- mWindowAdded = false;
- // Rethrow the exception to preserve the existing behavior. Some IMEs may have directly
- // called this method and relied on this exception for some clean-up tasks.
- // TODO: Give developers a clear guideline of whether it's OK to call this method or
- // InputMethodService#requestShowSelf(int) should always be used instead.
- throw e;
- } finally {
- // TODO: Is it OK to set true when we get BadTokenException?
- mWindowWasVisible = true;
- mInShowWindow = false;
- }
+
+ mWindowWasVisible = mWindowVisible;
+ mInShowWindow = true;
+ showWindowInner(showInput);
+ mWindowWasVisible = true;
+ mInShowWindow = false;
}
void showWindowInner(boolean showInput) {
@@ -1825,9 +1792,8 @@
initialize();
updateFullscreenMode();
updateInputViewShown();
-
- if (!mWindowAdded || !mWindowCreated) {
- mWindowAdded = true;
+
+ if (!mWindowCreated) {
mWindowCreated = true;
initialize();
if (DEBUG) Log.v(TAG, "CALL: onCreateCandidatesView");
@@ -2852,8 +2818,7 @@
@Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
final Printer p = new PrintWriterPrinter(fout);
p.println("Input method service state for " + this + ":");
- p.println(" mWindowCreated=" + mWindowCreated
- + " mWindowAdded=" + mWindowAdded);
+ p.println(" mWindowCreated=" + mWindowCreated);
p.println(" mWindowVisible=" + mWindowVisible
+ " mWindowWasVisible=" + mWindowWasVisible
+ " mInShowWindow=" + mInShowWindow);
diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java
index 795117e..b4b8887 100644
--- a/core/java/android/inputmethodservice/SoftInputWindow.java
+++ b/core/java/android/inputmethodservice/SoftInputWindow.java
@@ -16,15 +16,22 @@
package android.inputmethodservice;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Rect;
+import android.os.Debug;
import android.os.IBinder;
+import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.WindowManager;
+import java.lang.annotation.Retention;
+
/**
* A SoftInputWindow is a Dialog that is intended to be used for a top-level input
* method window. It will be displayed along the edge of the screen, moving
@@ -33,6 +40,9 @@
* @hide
*/
public class SoftInputWindow extends Dialog {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "SoftInputWindow";
+
final String mName;
final Callback mCallback;
final KeyEvent.Callback mKeyEventCallback;
@@ -42,16 +52,65 @@
final boolean mTakesFocus;
private final Rect mBounds = new Rect();
+ @Retention(SOURCE)
+ @IntDef(value = {SoftInputWindowState.TOKEN_PENDING, SoftInputWindowState.TOKEN_SET,
+ SoftInputWindowState.SHOWN_AT_LEAST_ONCE, SoftInputWindowState.REJECTED_AT_LEAST_ONCE})
+ private @interface SoftInputWindowState {
+ /**
+ * The window token is not set yet.
+ */
+ int TOKEN_PENDING = 0;
+ /**
+ * The window token was set, but the window is not shown yet.
+ */
+ int TOKEN_SET = 1;
+ /**
+ * The window was shown at least once.
+ */
+ int SHOWN_AT_LEAST_ONCE = 2;
+ /**
+ * {@link android.view.WindowManager.BadTokenException} was sent when calling
+ * {@link Dialog#show()} at least once.
+ */
+ int REJECTED_AT_LEAST_ONCE = 3;
+ /**
+ * The window is considered destroyed. Any incoming request should be ignored.
+ */
+ int DESTROYED = 4;
+ }
+
+ @SoftInputWindowState
+ private int mWindowState = SoftInputWindowState.TOKEN_PENDING;
+
public interface Callback {
public void onBackPressed();
}
public void setToken(IBinder token) {
- WindowManager.LayoutParams lp = getWindow().getAttributes();
- lp.token = token;
- getWindow().setAttributes(lp);
+ switch (mWindowState) {
+ case SoftInputWindowState.TOKEN_PENDING:
+ // Normal scenario. Nothing to worry about.
+ WindowManager.LayoutParams lp = getWindow().getAttributes();
+ lp.token = token;
+ getWindow().setAttributes(lp);
+ updateWindowState(SoftInputWindowState.TOKEN_SET);
+ return;
+ case SoftInputWindowState.TOKEN_SET:
+ case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
+ case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
+ throw new IllegalStateException("setToken can be called only once");
+ case SoftInputWindowState.DESTROYED:
+ // Just ignore. Since there are multiple event queues from the token is issued
+ // in the system server to the timing when it arrives here, it can be delivered
+ // after the is already destroyed. No one should be blamed because of such an
+ // unfortunate but possible scenario.
+ Log.i(TAG, "Ignoring setToken() because window is already destroyed.");
+ return;
+ default:
+ throw new IllegalStateException("Unexpected state=" + mWindowState);
+ }
}
-
+
/**
* Create a SoftInputWindow that uses a custom style.
*
@@ -190,4 +249,109 @@
getWindow().setFlags(windowSetFlags, windowModFlags);
}
+
+ @Override
+ public final void show() {
+ switch (mWindowState) {
+ case SoftInputWindowState.TOKEN_PENDING:
+ throw new IllegalStateException("Window token is not set yet.");
+ case SoftInputWindowState.TOKEN_SET:
+ case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
+ // Normal scenario. Nothing to worry about.
+ try {
+ super.show();
+ updateWindowState(SoftInputWindowState.SHOWN_AT_LEAST_ONCE);
+ } catch (WindowManager.BadTokenException e) {
+ // Just ignore this exception. Since show() can be requested from other
+ // components such as the system and there could be multiple event queues before
+ // the request finally arrives here, the system may have already invalidated the
+ // window token attached to our window. In such a scenario, receiving
+ // BadTokenException here is an expected behavior. We just ignore it and update
+ // the state so that we do not touch this window later.
+ Log.i(TAG, "Probably the IME window token is already invalidated."
+ + " show() does nothing.");
+ updateWindowState(SoftInputWindowState.REJECTED_AT_LEAST_ONCE);
+ }
+ return;
+ case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
+ // Just ignore. In general we cannot completely avoid this kind of race condition.
+ Log.i(TAG, "Not trying to call show() because it was already rejected once.");
+ return;
+ case SoftInputWindowState.DESTROYED:
+ // Just ignore. In general we cannot completely avoid this kind of race condition.
+ Log.i(TAG, "Ignoring show() because the window is already destroyed.");
+ return;
+ default:
+ throw new IllegalStateException("Unexpected state=" + mWindowState);
+ }
+ }
+
+ final void dismissForDestroyIfNecessary() {
+ switch (mWindowState) {
+ case SoftInputWindowState.TOKEN_PENDING:
+ case SoftInputWindowState.TOKEN_SET:
+ // nothing to do because the window has never been shown.
+ updateWindowState(SoftInputWindowState.DESTROYED);
+ return;
+ case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
+ // Disable exit animation for the current IME window
+ // to avoid the race condition between the exit and enter animations
+ // when the current IME is being switched to another one.
+ try {
+ getWindow().setWindowAnimations(0);
+ dismiss();
+ } catch (WindowManager.BadTokenException e) {
+ // Just ignore this exception. Since show() can be requested from other
+ // components such as the system and there could be multiple event queues before
+ // the request finally arrives here, the system may have already invalidated the
+ // window token attached to our window. In such a scenario, receiving
+ // BadTokenException here is an expected behavior. We just ignore it and update
+ // the state so that we do not touch this window later.
+ Log.i(TAG, "Probably the IME window token is already invalidated. "
+ + "No need to dismiss it.");
+ }
+ // Either way, consider that the window is destroyed.
+ updateWindowState(SoftInputWindowState.DESTROYED);
+ return;
+ case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
+ // Just ignore. In general we cannot completely avoid this kind of race condition.
+ Log.i(TAG,
+ "Not trying to dismiss the window because it is most likely unnecessary.");
+ // Anyway, consider that the window is destroyed.
+ updateWindowState(SoftInputWindowState.DESTROYED);
+ return;
+ case SoftInputWindowState.DESTROYED:
+ throw new IllegalStateException(
+ "dismissForDestroyIfNecessary can be called only once");
+ default:
+ throw new IllegalStateException("Unexpected state=" + mWindowState);
+ }
+ }
+
+ private void updateWindowState(@SoftInputWindowState int newState) {
+ if (DEBUG) {
+ if (mWindowState != newState) {
+ Log.d(TAG, "WindowState: " + stateToString(mWindowState) + " -> "
+ + stateToString(newState) + " @ " + Debug.getCaller());
+ }
+ }
+ mWindowState = newState;
+ }
+
+ private static String stateToString(@SoftInputWindowState int state) {
+ switch (state) {
+ case SoftInputWindowState.TOKEN_PENDING:
+ return "TOKEN_PENDING";
+ case SoftInputWindowState.TOKEN_SET:
+ return "TOKEN_SET";
+ case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:
+ return "SHOWN_AT_LEAST_ONCE";
+ case SoftInputWindowState.REJECTED_AT_LEAST_ONCE:
+ return "REJECTED_AT_LEAST_ONCE";
+ case SoftInputWindowState.DESTROYED:
+ return "DESTROYED";
+ default:
+ throw new IllegalStateException("Unknown state=" + state);
+ }
+ }
}
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d733207..365e4a4 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -2881,55 +2881,55 @@
<!-- Title for EditText context menu [CHAR LIMIT=20] -->
<string name="editTextMenuTitle">Text actions</string>
- <!-- Label for item in the text selection menu to trigger an Email app [CHAR LIMIT=20] -->
+ <!-- Label for item in the text selection menu to trigger an Email app. Should be a verb. [CHAR LIMIT=20] -->
<string name="email">Email</string>
<!-- Accessibility description for an item in the text selection menu to trigger an Email app [CHAR LIMIT=NONE] -->
<string name="email_desc">Email selected address</string>
- <!-- Label for item in the text selection menu to trigger a Dialer app [CHAR LIMIT=20] -->
+ <!-- Label for item in the text selection menu to trigger a Dialer app. Should be a verb. [CHAR LIMIT=20] -->
<string name="dial">Call</string>
<!-- Accessibility description for an item in the text selection menu to call a phone number [CHAR LIMIT=NONE] -->
<string name="dial_desc">Call selected phone number</string>
- <!-- Label for item in the text selection menu to trigger a Map app [CHAR LIMIT=20] -->
+ <!-- Label for item in the text selection menu to trigger a Map app. Should be a verb. [CHAR LIMIT=20] -->
<string name="map">Map</string>
<!-- Accessibility description for an item in the text selection menu to open maps for an address [CHAR LIMIT=NONE] -->
<string name="map_desc">Locate selected address</string>
- <!-- Label for item in the text selection menu to trigger a Browser app [CHAR LIMIT=20] -->
+ <!-- Label for item in the text selection menu to trigger a Browser app. Should be a verb. [CHAR LIMIT=20] -->
<string name="browse">Open</string>
<!-- Accessibility description for an item in the text selection menu to open a URL in a browser [CHAR LIMIT=NONE] -->
<string name="browse_desc">Open selected URL</string>
- <!-- Label for item in the text selection menu to trigger an SMS app [CHAR LIMIT=20] -->
+ <!-- Label for item in the text selection menu to trigger an SMS app. Should be a verb. [CHAR LIMIT=20] -->
<string name="sms">Message</string>
<!-- Accessibility description for an item in the text selection menu to send an SMS to a phone number [CHAR LIMIT=NONE] -->
<string name="sms_desc">Message selected phone number</string>
- <!-- Label for item in the text selection menu to trigger adding a contact [CHAR LIMIT=20] -->
+ <!-- Label for item in the text selection menu to trigger adding a contact. Should be a verb. [CHAR LIMIT=20] -->
<string name="add_contact">Add</string>
<!-- Accessibility description for an item in the text selection menu to add the selected detail to contacts [CHAR LIMIT=NONE] -->
<string name="add_contact_desc">Add to contacts</string>
- <!-- Label for item in the text selection menu to view the calendar for the selected time/date [CHAR LIMIT=20] -->
+ <!-- Label for item in the text selection menu to view the calendar for the selected time/date. Should be a verb. [CHAR LIMIT=20] -->
<string name="view_calendar">View</string>
<!-- Accessibility description for an item in the text selection menu to view the calendar for a date [CHAR LIMIT=NONE]-->
<string name="view_calendar_desc">View selected time in calendar</string>
- <!-- Label for item in the text selection menu to create a calendar event at the selected time/date [CHAR LIMIT=20] -->
+ <!-- Label for item in the text selection menu to create a calendar event at the selected time/date. Should be a verb. [CHAR LIMIT=20] -->
<string name="add_calendar_event">Schedule</string>
<!-- Accessibility description for an item in the text selection menu to schedule an event for a date [CHAR LIMIT=NONE] -->
<string name="add_calendar_event_desc">Schedule event for selected time</string>
- <!-- Label for item in the text selection menu to track a selected flight number [CHAR LIMIT=20] -->
+ <!-- Label for item in the text selection menu to track a selected flight number. Should be a verb. [CHAR LIMIT=20] -->
<string name="view_flight">Track</string>
<!-- Accessibility description for an item in the text selection menu to track a flight [CHAR LIMIT=NONE] -->
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index 2be9311..7c81399 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -106,6 +106,14 @@
</intent-filter>
</receiver>
+ <receiver android:name=".PackageInstalledReceiver"
+ android:exported="true">
+ <intent-filter android:priority="1">
+ <action android:name="android.intent.action.PACKAGE_ADDED" />
+ <data android:scheme="package" />
+ </intent-filter>
+ </receiver>
+
<activity android:name=".UninstallUninstalling"
android:excludeFromRecents="true"
android:exported="false" />
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java
new file mode 100644
index 0000000..67ac99f
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 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.packageinstaller;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Receive new app installed broadcast and notify user new app installed.
+ */
+public class PackageInstalledReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "PackageInstalledReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // TODO: Add logic to handle new app installed.
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 7cb22a3..03febda 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -101,12 +101,7 @@
if (savedInstanceState != null) {
setExpanded(savedInstanceState.getBoolean(EXTRA_EXPANDED));
setListening(savedInstanceState.getBoolean(EXTRA_LISTENING));
- int[] loc = new int[2];
- View edit = view.findViewById(android.R.id.edit);
- edit.getLocationInWindow(loc);
- int x = loc[0] + edit.getWidth() / 2;
- int y = loc[1] + edit.getHeight() / 2;
- mQSCustomizer.setEditLocation(x, y);
+ setEditLocation(view);
mQSCustomizer.restoreInstanceState(savedInstanceState);
}
SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this);
@@ -161,15 +156,24 @@
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
+ setEditLocation(getView());
if (newConfig.getLayoutDirection() != mLayoutDirection) {
mLayoutDirection = newConfig.getLayoutDirection();
-
if (mQSAnimator != null) {
mQSAnimator.onRtlChanged();
}
}
}
+ private void setEditLocation(View view) {
+ Log.w(TAG, "I'm changing the location of the button!!!");
+ View edit = view.findViewById(android.R.id.edit);
+ int[] loc = edit.getLocationOnScreen();
+ int x = loc[0] + edit.getWidth() / 2;
+ int y = loc[1] + edit.getHeight() / 2;
+ mQSCustomizer.setEditLocation(x, y);
+ }
+
@Override
public void setContainer(ViewGroup container) {
if (container instanceof NotificationsQuickSettingsContainer) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 6c330b0..762fd75 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -29,6 +29,7 @@
import android.os.Message;
import android.service.quicksettings.Tile;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
@@ -60,6 +61,8 @@
public static final String QS_SHOW_BRIGHTNESS = "qs_show_brightness";
public static final String QS_SHOW_HEADER = "qs_show_header";
+ private static final String TAG = "QSPanel";
+
protected final Context mContext;
protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
protected final View mBrightnessView;
@@ -313,7 +316,7 @@
public void onCollapse() {
if (mCustomizePanel != null && mCustomizePanel.isShown()) {
- mCustomizePanel.hide(mCustomizePanel.getWidth() / 2, mCustomizePanel.getHeight() / 2);
+ mCustomizePanel.hide();
}
}
@@ -480,8 +483,7 @@
public void run() {
if (mCustomizePanel != null) {
if (!mCustomizePanel.isCustomizing()) {
- int[] loc = new int[2];
- v.getLocationInWindow(loc);
+ int[] loc = v.getLocationOnScreen();
int x = loc[0] + v.getWidth() / 2;
int y = loc[1] + v.getHeight() / 2;
mCustomizePanel.show(x, y);
@@ -495,7 +497,7 @@
public void closeDetail() {
if (mCustomizePanel != null && mCustomizePanel.isShown()) {
// Treat this as a detail panel for now, to make things easy.
- mCustomizePanel.hide(mCustomizePanel.getWidth() / 2, mCustomizePanel.getHeight() / 2);
+ mCustomizePanel.hide();
return;
}
showDetail(false, mDetailRecord);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
index 2ea15bd..3f7eeb8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -26,6 +26,7 @@
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.util.AttributeSet;
+import android.util.Log;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@@ -63,6 +64,7 @@
private static final int MENU_RESET = Menu.FIRST;
private static final String EXTRA_QS_CUSTOMIZING = "qs_customizing";
+ private static final String TAG = "QSCustomizer";
private final QSDetailClipper mClipper;
private final LightBarController mLightBarController;
@@ -94,7 +96,7 @@
mToolbar.setNavigationOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- hide((int) v.getX() + v.getWidth() / 2, (int) v.getY() + v.getHeight() / 2);
+ hide();
}
});
mToolbar.setOnMenuItemClickListener(this);
@@ -154,16 +156,20 @@
mQs = qs;
}
+ /** Animate and show QSCustomizer panel.
+ * @param x,y Location on screen of {@code edit} button to determine center of animation.
+ */
public void show(int x, int y) {
if (!isShown) {
- mX = x;
- mY = y;
+ int containerLocation[] = findViewById(R.id.customize_container).getLocationOnScreen();
+ mX = x - containerLocation[0];
+ mY = y - containerLocation[1];
MetricsLogger.visible(getContext(), MetricsProto.MetricsEvent.QS_EDIT);
isShown = true;
mOpening = true;
setTileSpecs();
setVisibility(View.VISIBLE);
- mClipper.animateCircularClip(x, y, true, mExpandAnimationListener);
+ mClipper.animateCircularClip(mX, mY, true, mExpandAnimationListener);
queryTiles();
mNotifQsContainer.setCustomizerAnimating(true);
mNotifQsContainer.setCustomizerShowing(true);
@@ -192,7 +198,7 @@
mTileQueryHelper.queryTiles(mHost);
}
- public void hide(int x, int y) {
+ public void hide() {
if (isShown) {
MetricsLogger.hidden(getContext(), MetricsProto.MetricsEvent.QS_EDIT);
isShown = false;
@@ -278,16 +284,18 @@
});
}
}
-
+ /** @param x,y Location on screen of animation center.
+ */
public void setEditLocation(int x, int y) {
- mX = x;
- mY = y;
+ int containerLocation[] = findViewById(R.id.customize_container).getLocationOnScreen();
+ mX = x - containerLocation[0];
+ mY = y - containerLocation[1];
}
private final Callback mKeyguardCallback = () -> {
if (!isAttachedToWindow()) return;
if (Dependency.get(KeyguardMonitor.class).isShowing() && !mOpening) {
- hide(0, 0);
+ hide();
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java
index c017104..35e9d55 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java
@@ -36,10 +36,20 @@
* remove notifications that appear on screen for a period of time and dismiss themselves at the
* appropriate time. These include heads up notifications and ambient pulses.
*/
-public abstract class AlertingNotificationManager {
+public abstract class AlertingNotificationManager implements NotificationLifetimeExtender {
private static final String TAG = "AlertNotifManager";
protected final Clock mClock = new Clock();
protected final ArrayMap<String, AlertEntry> mAlertEntries = new ArrayMap<>();
+
+ /**
+ * This is the list of entries that have already been removed from the
+ * NotificationManagerService side, but we keep it to prevent the UI from looking weird and
+ * will remove when possible. See {@link NotificationLifetimeExtender}
+ */
+ protected final ArraySet<NotificationData.Entry> mExtendedLifetimeAlertEntries =
+ new ArraySet<>();
+
+ protected NotificationSafeToRemoveCallback mNotificationLifetimeFinishedCallback;
protected int mMinimumDisplayTime;
protected int mAutoDismissNotificationDecay;
@VisibleForTesting
@@ -74,7 +84,7 @@
if (alertEntry == null) {
return true;
}
- if (releaseImmediately || alertEntry.wasShownLongEnough()) {
+ if (releaseImmediately || canRemoveImmediately(key)) {
removeAlertEntry(key);
} else {
alertEntry.removeAsSoonAsPossible();
@@ -191,6 +201,12 @@
onAlertEntryRemoved(alertEntry);
entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
alertEntry.reset();
+ if (mExtendedLifetimeAlertEntries.contains(entry)) {
+ if (mNotificationLifetimeFinishedCallback != null) {
+ mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
+ }
+ mExtendedLifetimeAlertEntries.remove(entry);
+ }
}
/**
@@ -207,6 +223,40 @@
return new AlertEntry();
}
+ /**
+ * Whether or not the alert can be removed currently. If it hasn't been on screen long enough
+ * it should not be removed unless forced
+ * @param key the key to check if removable
+ * @return true if the alert entry can be removed
+ */
+ protected boolean canRemoveImmediately(String key) {
+ AlertEntry alertEntry = mAlertEntries.get(key);
+ return alertEntry == null || alertEntry.wasShownLongEnough();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // NotificationLifetimeExtender Methods
+
+ @Override
+ public void setCallback(NotificationSafeToRemoveCallback callback) {
+ mNotificationLifetimeFinishedCallback = callback;
+ }
+
+ @Override
+ public boolean shouldExtendLifetime(NotificationData.Entry entry) {
+ return !canRemoveImmediately(entry.key);
+ }
+
+ @Override
+ public void setShouldExtendLifetime(NotificationData.Entry entry, boolean shouldExtend) {
+ if (shouldExtend) {
+ mExtendedLifetimeAlertEntries.add(entry);
+ } else {
+ mExtendedLifetimeAlertEntries.remove(entry);
+ }
+ }
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+
protected class AlertEntry implements Comparable<AlertEntry> {
@Nullable public NotificationData.Entry mEntry;
public long mPostTime;
@@ -214,11 +264,11 @@
@Nullable protected Runnable mRemoveAlertRunnable;
- public void setEntry(@Nullable final NotificationData.Entry entry) {
+ public void setEntry(@NonNull final NotificationData.Entry entry) {
setEntry(entry, () -> removeAlertEntry(entry.key));
}
- public void setEntry(@Nullable final NotificationData.Entry entry,
+ public void setEntry(@NonNull final NotificationData.Entry entry,
@Nullable Runnable removeAlertRunnable) {
mEntry = entry;
mRemoveAlertRunnable = removeAlertRunnable;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLifetimeExtender.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLifetimeExtender.java
new file mode 100644
index 0000000..42e380f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLifetimeExtender.java
@@ -0,0 +1,53 @@
+package com.android.systemui.statusbar;
+
+import com.android.systemui.statusbar.notification.NotificationData;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Interface for anything that may need to keep notifications managed even after
+ * {@link NotificationListener} removes it. The lifetime extender is in charge of performing the
+ * callback when the notification is then safe to remove.
+ */
+public interface NotificationLifetimeExtender {
+
+ /**
+ * Set the handler to callback to when the notification is safe to remove.
+ *
+ * @param callback the handler to callback
+ */
+ void setCallback(@NonNull NotificationSafeToRemoveCallback callback);
+
+ /**
+ * Determines whether or not the extender needs the notification kept after removal.
+ *
+ * @param entry the entry containing the notification to check
+ * @return true if the notification lifetime should be extended
+ */
+ boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry);
+
+ /**
+ * Sets whether or not the lifetime should be extended. In practice, if shouldExtend is
+ * true, this is where the extender starts managing the entry internally and is now
+ * responsible for calling {@link NotificationSafeToRemoveCallback#onSafeToRemove(String)} when
+ * the entry is safe to remove. If shouldExtend is false, the extender no longer needs to
+ * worry about it (either because we will be removing it anyway or the entry is no longer
+ * removed due to an update).
+ *
+ * @param entry the entry to mark as having an extended lifetime
+ * @param shouldExtend true if the extender should manage the entry now, false otherwise
+ */
+ void setShouldExtendLifetime(@NonNull NotificationData.Entry entry, boolean shouldExtend);
+
+ /**
+ * The callback for when the notification is now safe to remove (i.e. its lifetime has ended).
+ */
+ interface NotificationSafeToRemoveCallback {
+ /**
+ * Called when the lifetime extender determines it's safe to remove.
+ *
+ * @param key key of the entry that is now safe to remove
+ */
+ void onSafeToRemove(String key);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
index 9b375df..cfa09bc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
@@ -76,7 +76,6 @@
mPresenter.getHandler().post(() -> {
processForRemoteInput(sbn.getNotification(), mContext);
String key = sbn.getKey();
- mEntryManager.removeKeyKeptForRemoteInput(key);
boolean isUpdate =
mEntryManager.getNotificationData().get(key) != null;
// In case we don't allow child notifications, we ignore children of
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 929713c..1a3e812 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -17,8 +17,10 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
+import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityOptions;
+import android.app.Notification;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Context;
@@ -29,6 +31,7 @@
import android.os.SystemProperties;
import android.os.UserManager;
import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.view.MotionEvent;
@@ -50,6 +53,7 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Set;
/**
@@ -61,10 +65,10 @@
public class NotificationRemoteInputManager implements Dumpable {
public static final boolean ENABLE_REMOTE_INPUT =
SystemProperties.getBoolean("debug.enable_remote_input", true);
- public static final boolean FORCE_REMOTE_INPUT_HISTORY =
+ public static boolean FORCE_REMOTE_INPUT_HISTORY =
SystemProperties.getBoolean("debug.force_remoteinput_history", true);
private static final boolean DEBUG = false;
- private static final String TAG = "NotificationRemoteInputManager";
+ private static final String TAG = "NotifRemoteInputManager";
/**
* How long to wait before auto-dismissing a notification that was kept for remote input, and
@@ -74,12 +78,25 @@
*/
private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
- protected final ArraySet<NotificationData.Entry> mRemoteInputEntriesToRemoveOnCollapse =
+ /**
+ * Notifications that are already removed but are kept around because we want to show the
+ * remote input history. See {@link RemoteInputHistoryExtender} and
+ * {@link SmartReplyHistoryExtender}.
+ */
+ protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>();
+
+ /**
+ * Notifications that are already removed but are kept around because the remote input is
+ * actively being used (i.e. user is typing in it). See {@link RemoteInputActiveExtender}.
+ */
+ protected final ArraySet<NotificationData.Entry> mEntriesKeptForRemoteInputActive =
new ArraySet<>();
// Dependencies:
protected final NotificationLockscreenUserManager mLockscreenUserManager =
Dependency.get(NotificationLockscreenUserManager.class);
+ protected final SmartReplyController mSmartReplyController =
+ Dependency.get(SmartReplyController.class);
protected final Context mContext;
private final UserManager mUserManager;
@@ -87,8 +104,11 @@
protected RemoteInputController mRemoteInputController;
protected NotificationPresenter mPresenter;
protected NotificationEntryManager mEntryManager;
+ protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
+ mNotificationLifetimeFinishedCallback;
protected IStatusBarService mBarService;
protected Callback mCallback;
+ protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
@@ -276,6 +296,7 @@
mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ addLifetimeExtenders();
}
public void setUpWithPresenter(NotificationPresenter presenter,
@@ -290,16 +311,16 @@
@Override
public void onRemoteInputSent(NotificationData.Entry entry) {
if (FORCE_REMOTE_INPUT_HISTORY
- && mEntryManager.isNotificationKeptForRemoteInput(entry.key)) {
- mEntryManager.removeNotification(entry.key, null);
- } else if (mRemoteInputEntriesToRemoveOnCollapse.contains(entry)) {
+ && isNotificationKeptForRemoteInputHistory(entry.key)) {
+ mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
+ } else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
// We're currently holding onto this notification, but from the apps point of
// view it is already canceled, so we'll need to cancel it on the apps behalf
// after sending - unless the app posts an update in the mean time, so wait a
// bit.
mPresenter.getHandler().postDelayed(() -> {
- if (mRemoteInputEntriesToRemoveOnCollapse.remove(entry)) {
- mEntryManager.removeNotification(entry.key, null);
+ if (mEntriesKeptForRemoteInputActive.remove(entry)) {
+ mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
}
}, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
}
@@ -310,45 +331,74 @@
}
}
});
+ mSmartReplyController.setCallback((entry, reply) -> {
+ StatusBarNotification newSbn =
+ rebuildNotificationWithRemoteInput(entry, reply, true /* showSpinner */);
+ mEntryManager.updateNotification(newSbn, null /* ranking */);
+ });
+ }
+ /**
+ * Adds all the notification lifetime extenders. Each extender represents a reason for the
+ * NotificationRemoteInputManager to keep a notification lifetime extended.
+ */
+ protected void addLifetimeExtenders() {
+ mLifetimeExtenders.add(new RemoteInputHistoryExtender());
+ mLifetimeExtenders.add(new SmartReplyHistoryExtender());
+ mLifetimeExtenders.add(new RemoteInputActiveExtender());
+ }
+
+ public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
+ return mLifetimeExtenders;
}
public RemoteInputController getController() {
return mRemoteInputController;
}
- public void onUpdateNotification(NotificationData.Entry entry) {
- mRemoteInputEntriesToRemoveOnCollapse.remove(entry);
- }
-
- /**
- * Returns true if NotificationRemoteInputManager wants to keep this notification around.
- *
- * @param entry notification being removed
- */
- public boolean onRemoveNotification(NotificationData.Entry entry) {
- if (entry != null && mRemoteInputController.isRemoteInputActive(entry)
- && (entry.row != null && !entry.row.isDismissed())) {
- mRemoteInputEntriesToRemoveOnCollapse.add(entry);
- return true;
- }
- return false;
- }
-
public void onPerformRemoveNotification(StatusBarNotification n,
NotificationData.Entry entry) {
+ if (mKeysKeptForRemoteInputHistory.contains(n.getKey())) {
+ mKeysKeptForRemoteInputHistory.remove(n.getKey());
+ }
if (mRemoteInputController.isRemoteInputActive(entry)) {
mRemoteInputController.removeRemoteInput(entry, null);
}
}
- public void removeRemoteInputEntriesKeptUntilCollapsed() {
- for (int i = 0; i < mRemoteInputEntriesToRemoveOnCollapse.size(); i++) {
- NotificationData.Entry entry = mRemoteInputEntriesToRemoveOnCollapse.valueAt(i);
+ public void onPanelCollapsed() {
+ for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
+ NotificationData.Entry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
mRemoteInputController.removeRemoteInput(entry, null);
- mEntryManager.removeNotification(entry.key, mEntryManager.getLatestRankingMap());
+ if (mNotificationLifetimeFinishedCallback != null) {
+ mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
+ }
}
- mRemoteInputEntriesToRemoveOnCollapse.clear();
+ mEntriesKeptForRemoteInputActive.clear();
+ }
+
+ public boolean isNotificationKeptForRemoteInputHistory(String key) {
+ return mKeysKeptForRemoteInputHistory.contains(key);
+ }
+
+ public boolean shouldKeepForRemoteInputHistory(NotificationData.Entry entry) {
+ if (entry.row == null || entry.row.isDismissed()) {
+ return false;
+ }
+ if (!FORCE_REMOTE_INPUT_HISTORY) {
+ return false;
+ }
+ return (mRemoteInputController.isSpinning(entry.key) || entry.hasJustSentRemoteInput());
+ }
+
+ public boolean shouldKeepForSmartReplyHistory(NotificationData.Entry entry) {
+ if (entry.row == null || entry.row.isDismissed()) {
+ return false;
+ }
+ if (!FORCE_REMOTE_INPUT_HISTORY) {
+ return false;
+ }
+ return mSmartReplyController.isSendingSmartReply(entry.key);
}
public void checkRemoteInputOutside(MotionEvent event) {
@@ -359,11 +409,63 @@
}
}
+ @VisibleForTesting
+ StatusBarNotification rebuildNotificationForCanceledSmartReplies(
+ NotificationData.Entry entry) {
+ return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
+ false /* showSpinner */);
+ }
+
+ @VisibleForTesting
+ StatusBarNotification rebuildNotificationWithRemoteInput(NotificationData.Entry entry,
+ CharSequence remoteInputText, boolean showSpinner) {
+ StatusBarNotification sbn = entry.notification;
+
+ Notification.Builder b = Notification.Builder
+ .recoverBuilder(mContext, sbn.getNotification().clone());
+ if (remoteInputText != null) {
+ CharSequence[] oldHistory = sbn.getNotification().extras
+ .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
+ CharSequence[] newHistory;
+ if (oldHistory == null) {
+ newHistory = new CharSequence[1];
+ } else {
+ newHistory = new CharSequence[oldHistory.length + 1];
+ System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
+ }
+ newHistory[0] = String.valueOf(remoteInputText);
+ b.setRemoteInputHistory(newHistory);
+ }
+ b.setShowRemoteInputSpinner(showSpinner);
+ b.setHideSmartReplies(true);
+
+ Notification newNotification = b.build();
+
+ // Undo any compatibility view inflation
+ newNotification.contentView = sbn.getNotification().contentView;
+ newNotification.bigContentView = sbn.getNotification().bigContentView;
+ newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
+
+ return new StatusBarNotification(
+ sbn.getPackageName(),
+ sbn.getOpPkg(),
+ sbn.getId(),
+ sbn.getTag(),
+ sbn.getUid(),
+ sbn.getInitialPid(),
+ newNotification,
+ sbn.getUser(),
+ sbn.getOverrideGroupKey(),
+ sbn.getPostTime());
+ }
+
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("NotificationRemoteInputManager state:");
- pw.print(" mRemoteInputEntriesToRemoveOnCollapse: ");
- pw.println(mRemoteInputEntriesToRemoveOnCollapse);
+ pw.print(" mKeysKeptForRemoteInputHistory: ");
+ pw.println(mKeysKeptForRemoteInputHistory);
+ pw.print(" mEntriesKeptForRemoteInputActive: ");
+ pw.println(mEntriesKeptForRemoteInputActive);
}
public void bindRow(ExpandableNotificationRow row) {
@@ -372,8 +474,133 @@
}
@VisibleForTesting
- public Set<NotificationData.Entry> getRemoteInputEntriesToRemoveOnCollapse() {
- return mRemoteInputEntriesToRemoveOnCollapse;
+ public Set<NotificationData.Entry> getEntriesKeptForRemoteInputActive() {
+ return mEntriesKeptForRemoteInputActive;
+ }
+
+ /**
+ * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended
+ * so we implement multiple NotificationLifetimeExtenders
+ */
+ protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
+ @Override
+ public void setCallback(NotificationSafeToRemoveCallback callback) {
+ if (mNotificationLifetimeFinishedCallback == null) {
+ mNotificationLifetimeFinishedCallback = callback;
+ }
+ }
+ }
+
+ /**
+ * Notification is kept alive as it was cancelled in response to a remote input interaction.
+ * This allows us to show what you replied and allows you to continue typing into it.
+ */
+ protected class RemoteInputHistoryExtender extends RemoteInputExtender {
+ @Override
+ public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
+ return shouldKeepForRemoteInputHistory(entry);
+ }
+
+ @Override
+ public void setShouldExtendLifetime(NotificationData.Entry entry,
+ boolean shouldExtend) {
+ if (shouldExtend) {
+ CharSequence remoteInputText = entry.remoteInputText;
+ if (TextUtils.isEmpty(remoteInputText)) {
+ remoteInputText = entry.remoteInputTextWhenReset;
+ }
+ StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry,
+ remoteInputText, false /* showSpinner */);
+ entry.onRemoteInputInserted();
+
+ if (newSbn == null) {
+ return;
+ }
+
+ mEntryManager.updateNotification(newSbn, null);
+
+ // Ensure the entry hasn't already been removed. This can happen if there is an
+ // inflation exception while updating the remote history
+ if (entry.row == null || entry.row.isRemoved()) {
+ return;
+ }
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Keeping notification around after sending remote input "
+ + entry.key);
+ }
+
+ mKeysKeptForRemoteInputHistory.add(entry.key);
+ } else {
+ mKeysKeptForRemoteInputHistory.remove(entry.key);
+ }
+ }
+ }
+
+ /**
+ * Notification is kept alive for smart reply history. Similar to REMOTE_INPUT_HISTORY but with
+ * {@link SmartReplyController} specific logic
+ */
+ protected class SmartReplyHistoryExtender extends RemoteInputExtender {
+ @Override
+ public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
+ return shouldKeepForSmartReplyHistory(entry);
+ }
+
+ @Override
+ public void setShouldExtendLifetime(NotificationData.Entry entry,
+ boolean shouldExtend) {
+ if (shouldExtend) {
+ StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
+
+ if (newSbn == null) {
+ return;
+ }
+
+ mEntryManager.updateNotification(newSbn, null);
+
+ if (entry.row == null || entry.row.isRemoved()) {
+ return;
+ }
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Keeping notification around after sending smart reply "
+ + entry.key);
+ }
+
+ mKeysKeptForRemoteInputHistory.add(entry.key);
+ } else {
+ mKeysKeptForRemoteInputHistory.remove(entry.key);
+ mSmartReplyController.stopSending(entry);
+ }
+ }
+ }
+
+ /**
+ * Notification is kept alive because the user is still using the remote input
+ */
+ protected class RemoteInputActiveExtender extends RemoteInputExtender {
+ @Override
+ public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
+ if (entry.row == null || entry.row.isDismissed()) {
+ return false;
+ }
+ return mRemoteInputController.isRemoteInputActive(entry);
+ }
+
+ @Override
+ public void setShouldExtendLifetime(NotificationData.Entry entry,
+ boolean shouldExtend) {
+ if (shouldExtend) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Keeping notification around while remote input active "
+ + entry.key);
+ }
+ mEntriesKeptForRemoteInputActive.add(entry);
+ } else {
+ mEntriesKeptForRemoteInputActive.remove(entry);
+ }
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java
index e43c9e5..fb888dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java
@@ -33,20 +33,19 @@
public class SmartReplyController {
private IStatusBarService mBarService;
private Set<String> mSendingKeys = new ArraySet<>();
+ private Callback mCallback;
public SmartReplyController() {
mBarService = Dependency.get(IStatusBarService.class);
}
- public void smartReplySent(NotificationData.Entry entry, int replyIndex, CharSequence reply) {
- NotificationEntryManager notificationEntryManager
- = Dependency.get(NotificationEntryManager.class);
- StatusBarNotification newSbn =
- notificationEntryManager.rebuildNotificationWithRemoteInput(entry, reply,
- true /* showSpinner */);
- notificationEntryManager.updateNotification(newSbn, null /* ranking */);
- mSendingKeys.add(entry.key);
+ public void setCallback(Callback callback) {
+ mCallback = callback;
+ }
+ public void smartReplySent(NotificationData.Entry entry, int replyIndex, CharSequence reply) {
+ mCallback.onSmartReplySent(entry, reply);
+ mSendingKeys.add(entry.key);
try {
mBarService.onNotificationSmartReplySent(entry.notification.getKey(),
replyIndex);
@@ -77,4 +76,17 @@
mSendingKeys.remove(entry.notification.getKey());
}
}
+
+ /**
+ * Callback for any class that needs to do something in response to a smart reply being sent.
+ */
+ public interface Callback {
+ /**
+ * A smart reply has just been sent for a notification
+ *
+ * @param entry the entry for the notification
+ * @param reply the reply that was sent
+ */
+ void onSmartReplySent(NotificationData.Entry entry, CharSequence reply);
+ }
}
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 b655a6b..906bbb9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -37,7 +37,6 @@
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationStats;
import android.service.notification.StatusBarNotification;
-import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.EventLog;
@@ -58,6 +57,7 @@
import com.android.systemui.R;
import com.android.systemui.UiOffloadThread;
import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.statusbar.NotificationLifetimeExtender;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationMediaManager;
@@ -65,7 +65,6 @@
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationUiAdjustment;
import com.android.systemui.statusbar.NotificationUpdateHandler;
-import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.notification.row.NotificationInflater;
import com.android.systemui.statusbar.notification.row.RowInflaterTask;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -100,8 +99,6 @@
protected final Context mContext;
protected final HashMap<String, NotificationData.Entry> mPendingNotifications = new HashMap<>();
protected final NotificationClicker mNotificationClicker = new NotificationClicker();
- protected final ArraySet<NotificationData.Entry> mHeadsUpEntriesToRemoveOnSwitch =
- new ArraySet<>();
// Dependencies:
protected final NotificationLockscreenUserManager mLockscreenUserManager =
@@ -124,8 +121,6 @@
Dependency.get(ForegroundServiceController.class);
protected final NotificationListener mNotificationListener =
Dependency.get(NotificationListener.class);
- private final SmartReplyController mSmartReplyController =
- Dependency.get(SmartReplyController.class);
protected IStatusBarService mBarService;
protected NotificationPresenter mPresenter;
@@ -139,13 +134,9 @@
protected boolean mUseHeadsUp = false;
protected boolean mDisableNotificationAlerts;
protected NotificationListContainer mListContainer;
+ protected final ArrayList<NotificationLifetimeExtender> mNotificationLifetimeExtenders
+ = new ArrayList<>();
private ExpandableNotificationRow.OnAppOpsClickListener mOnAppOpsClickListener;
- /**
- * Notifications with keys in this set are not actually around anymore. We kept them around
- * when they were canceled in response to a remote input interaction. This allows us to show
- * what you replied and allows you to continue typing into it.
- */
- private final ArraySet<String> mKeysKeptForRemoteInput = new ArraySet<>();
private final class NotificationClicker implements View.OnClickListener {
@@ -198,14 +189,6 @@
}
};
- public NotificationListenerService.RankingMap getLatestRankingMap() {
- return mLatestRankingMap;
- }
-
- public void setLatestRankingMap(NotificationListenerService.RankingMap latestRankingMap) {
- mLatestRankingMap = latestRankingMap;
- }
-
public void setDisableNotificationAlerts(boolean disableNotificationAlerts) {
mDisableNotificationAlerts = disableNotificationAlerts;
mHeadsUpObserver.onChange(true);
@@ -215,18 +198,6 @@
mDeviceProvisionedController.removeCallback(mDeviceProvisionedListener);
}
- public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
- if (!isHeadsUp && mHeadsUpEntriesToRemoveOnSwitch.contains(entry)) {
- removeNotification(entry.key, getLatestRankingMap());
- mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
- if (mHeadsUpEntriesToRemoveOnSwitch.isEmpty()) {
- setLatestRankingMap(null);
- }
- } else {
- updateNotificationRanking(null);
- }
- }
-
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("NotificationEntryManager state:");
@@ -240,8 +211,6 @@
}
pw.print(" mUseHeadsUp=");
pw.println(mUseHeadsUp);
- pw.print(" mKeysKeptForRemoteInput: ");
- pw.println(mKeysKeptForRemoteInput);
}
public NotificationEntryManager(Context context) {
@@ -294,6 +263,14 @@
mHeadsUpObserver);
}
+ mNotificationLifetimeExtenders.add(mHeadsUpManager);
+ mNotificationLifetimeExtenders.add(mGutsManager);
+ mNotificationLifetimeExtenders.addAll(mRemoteInputManager.getLifetimeExtenders());
+
+ for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) {
+ extender.setCallback(key -> removeNotification(key, mLatestRankingMap));
+ }
+
mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
mHeadsUpObserver.onChange(true); // set up
@@ -397,11 +374,6 @@
true);
NotificationData.Entry entry = mNotificationData.get(n.getKey());
- if (FORCE_REMOTE_INPUT_HISTORY
- && mKeysKeptForRemoteInput.contains(n.getKey())) {
- mKeysKeptForRemoteInput.remove(n.getKey());
- }
-
mRemoteInputManager.onPerformRemoveNotification(n, entry);
final String pkg = n.getPackageName();
final String tag = n.getTag();
@@ -433,7 +405,7 @@
* WARNING: this will call back into us. Don't hold any locks.
*/
void handleNotificationError(StatusBarNotification n, String message) {
- removeNotification(n.getKey(), null);
+ removeNotificationInternal(n.getKey(), null, true /* forceRemove */);
try {
mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(),
n.getInitialPid(), message, n.getUserId());
@@ -487,7 +459,11 @@
@Override
public void removeNotification(String key, NotificationListenerService.RankingMap ranking) {
- boolean deferRemoval = false;
+ removeNotificationInternal(key, ranking, false /* forceRemove */);
+ }
+
+ private void removeNotificationInternal(String key,
+ @Nullable NotificationListenerService.RankingMap ranking, boolean forceRemove) {
abortExistingInflation(key);
if (mHeadsUpManager.contains(key)) {
// A cancel() in response to a remote input shouldn't be delayed, as it makes the
@@ -497,154 +473,53 @@
boolean ignoreEarliestRemovalTime = mRemoteInputManager.getController().isSpinning(key)
&& !FORCE_REMOTE_INPUT_HISTORY
|| !mVisualStabilityManager.isReorderingAllowed();
- deferRemoval = !mHeadsUpManager.removeNotification(key, ignoreEarliestRemovalTime);
+
+ // Attempt to remove notification.
+ mHeadsUpManager.removeNotification(key, ignoreEarliestRemovalTime);
}
- mMediaManager.onNotificationRemoved(key);
NotificationData.Entry entry = mNotificationData.get(key);
- if (FORCE_REMOTE_INPUT_HISTORY
- && shouldKeepForRemoteInput(entry)
- && entry.row != null && !entry.row.isDismissed()) {
- CharSequence remoteInputText = entry.remoteInputText;
- if (TextUtils.isEmpty(remoteInputText)) {
- remoteInputText = entry.remoteInputTextWhenReset;
- }
- StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry,
- remoteInputText, false /* showSpinner */);
- boolean updated = false;
- entry.onRemoteInputInserted();
- try {
- updateNotificationInternal(newSbn, null);
- updated = true;
- } catch (InflationException e) {
- deferRemoval = false;
- }
- if (updated) {
- Log.w(TAG, "Keeping notification around after sending remote input "+ entry.key);
- addKeyKeptForRemoteInput(entry.key);
- return;
- }
- }
- if (FORCE_REMOTE_INPUT_HISTORY
- && shouldKeepForSmartReply(entry)
- && entry.row != null && !entry.row.isDismissed()) {
- // Turn off the spinner and hide buttons when an app cancels the notification.
- StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
- boolean updated = false;
- try {
- updateNotificationInternal(newSbn, null);
- updated = true;
- } catch (InflationException e) {
- // Ignore just don't keep the notification around.
- }
- // Treat the reply as longer sending.
- mSmartReplyController.stopSending(entry);
- if (updated) {
- Log.w(TAG, "Keeping notification around after sending smart reply " + entry.key);
- addKeyKeptForRemoteInput(entry.key);
- return;
- }
- }
-
- // Actually removing notification so smart reply controller can forget about it.
- mSmartReplyController.stopSending(entry);
-
- if (deferRemoval) {
- mLatestRankingMap = ranking;
- mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key));
+ if (entry == null) {
+ mCallback.onNotificationRemoved(key, null /* old */);
return;
}
- if (mRemoteInputManager.onRemoveNotification(entry)) {
- mLatestRankingMap = ranking;
- return;
+ // If a manager needs to keep the notification around for whatever reason, we return early
+ // and keep the notification
+ if (!forceRemove) {
+ for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) {
+ if (extender.shouldExtendLifetime(entry)) {
+ mLatestRankingMap = ranking;
+ extender.setShouldExtendLifetime(entry, true /* shouldExtend */);
+ return;
+ }
+ }
}
- if (entry != null && mGutsManager.getExposedGuts() != null
- && mGutsManager.getExposedGuts() == entry.row.getGuts()
- && entry.row.getGuts() != null && !entry.row.getGuts().isLeavebehind()) {
- Log.w(TAG, "Keeping notification because it's showing guts. " + key);
- mLatestRankingMap = ranking;
- mGutsManager.setKeyToRemoveOnGutsClosed(key);
- return;
+ // At this point, we are guaranteed the notification will be removed
+
+ // Ensure any managers keeping the lifetime extended stop managing the entry
+ for (NotificationLifetimeExtender extender: mNotificationLifetimeExtenders) {
+ extender.setShouldExtendLifetime(entry, false /* shouldExtend */);
}
- if (entry != null) {
- mForegroundServiceController.removeNotification(entry.notification);
- }
+ mMediaManager.onNotificationRemoved(key);
+ mForegroundServiceController.removeNotification(entry.notification);
- if (entry != null && entry.row != null) {
+ if (entry.row != null) {
entry.row.setRemoved();
mListContainer.cleanUpViewState(entry.row);
}
+
// Let's remove the children if this was a summary
handleGroupSummaryRemoved(key);
+
StatusBarNotification old = removeNotificationViews(key, ranking);
mCallback.onNotificationRemoved(key, old);
}
- public StatusBarNotification rebuildNotificationWithRemoteInput(NotificationData.Entry entry,
- CharSequence remoteInputText, boolean showSpinner) {
- StatusBarNotification sbn = entry.notification;
-
- Notification.Builder b = Notification.Builder
- .recoverBuilder(mContext, sbn.getNotification().clone());
- if (remoteInputText != null) {
- CharSequence[] oldHistory = sbn.getNotification().extras
- .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
- CharSequence[] newHistory;
- if (oldHistory == null) {
- newHistory = new CharSequence[1];
- } else {
- newHistory = new CharSequence[oldHistory.length + 1];
- System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
- }
- newHistory[0] = String.valueOf(remoteInputText);
- b.setRemoteInputHistory(newHistory);
- }
- b.setShowRemoteInputSpinner(showSpinner);
- b.setHideSmartReplies(true);
-
- Notification newNotification = b.build();
-
- // Undo any compatibility view inflation
- newNotification.contentView = sbn.getNotification().contentView;
- newNotification.bigContentView = sbn.getNotification().bigContentView;
- newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
-
- StatusBarNotification newSbn = new StatusBarNotification(sbn.getPackageName(),
- sbn.getOpPkg(),
- sbn.getId(), sbn.getTag(), sbn.getUid(), sbn.getInitialPid(),
- newNotification, sbn.getUser(), sbn.getOverrideGroupKey(), sbn.getPostTime());
- return newSbn;
- }
-
- @VisibleForTesting
- StatusBarNotification rebuildNotificationForCanceledSmartReplies(
- NotificationData.Entry entry) {
- return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
- false /* showSpinner */);
- }
-
- private boolean shouldKeepForSmartReply(NotificationData.Entry entry) {
- return entry != null && mSmartReplyController.isSendingSmartReply(entry.key);
- }
-
- private boolean shouldKeepForRemoteInput(NotificationData.Entry entry) {
- if (entry == null) {
- return false;
- }
- if (mRemoteInputManager.getController().isSpinning(entry.key)) {
- return true;
- }
- if (entry.hasJustSentRemoteInput()) {
- return true;
- }
- return false;
- }
-
private StatusBarNotification removeNotificationViews(String key,
NotificationListenerService.RankingMap ranking) {
NotificationData.Entry entry = mNotificationData.remove(key, ranking);
@@ -683,9 +558,9 @@
NotificationData.Entry childEntry = row.getEntry();
boolean isForeground = (row.getStatusBarNotification().getNotification().flags
& Notification.FLAG_FOREGROUND_SERVICE) != 0;
- boolean keepForReply = FORCE_REMOTE_INPUT_HISTORY
- && (shouldKeepForRemoteInput(childEntry)
- || shouldKeepForSmartReply(childEntry));
+ boolean keepForReply =
+ mRemoteInputManager.shouldKeepForRemoteInputHistory(childEntry)
+ || mRemoteInputManager.shouldKeepForSmartReplyHistory(childEntry);
if (isForeground || keepForReply) {
// the child is a foreground service notification which we can't remove or it's
// a child we're keeping around for reply!
@@ -868,13 +743,11 @@
if (entry == null) {
return;
}
- mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
- mRemoteInputManager.onUpdateNotification(entry);
- mSmartReplyController.stopSending(entry);
- if (key.equals(mGutsManager.getKeyToRemoveOnGutsClosed())) {
- mGutsManager.setKeyToRemoveOnGutsClosed(null);
- Log.w(TAG, "Notification that was kept for guts was updated. " + key);
+ // Notification is updated so it is essentially re-added and thus alive again. Don't need
+ // to keep it's lifetime extended.
+ for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) {
+ extender.setShouldExtendLifetime(entry, false /* shouldExtend */);
}
Notification n = notification.getNotification();
@@ -1080,20 +953,6 @@
return mHeadsUpManager.contains(key);
}
- public boolean isNotificationKeptForRemoteInput(String key) {
- return mKeysKeptForRemoteInput.contains(key);
- }
-
- public void removeKeyKeptForRemoteInput(String key) {
- mKeysKeptForRemoteInput.remove(key);
- }
-
- public void addKeyKeptForRemoteInput(String key) {
- if (FORCE_REMOTE_INPUT_HISTORY) {
- mKeysKeptForRemoteInput.add(key);
- }
- }
-
/**
* Callback for NotificationEntryManager.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index e635976..a096baa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -38,27 +38,27 @@
import android.view.View;
import android.view.accessibility.AccessibilityManager;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
import com.android.systemui.Dependency;
import com.android.systemui.Dumpable;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.statusbar.NotificationLifetimeExtender;
+import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationPresenter;
-import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.statusbar.phone.StatusBar;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import androidx.annotation.VisibleForTesting;
-
/**
* Handles various NotificationGuts related tasks, such as binding guts to a row, opening and
* closing guts, and keeping track of the currently exposed notification guts.
*/
-public class NotificationGutsManager implements Dumpable {
+public class NotificationGutsManager implements Dumpable, NotificationLifetimeExtender {
private static final String TAG = "NotificationGutsManager";
// Must match constant in Settings. Used to highlight preferences when linking to Settings.
@@ -75,12 +75,13 @@
// which notification is currently being longpress-examined by the user
private NotificationGuts mNotificationGutsExposed;
private NotificationMenuRowPlugin.MenuItem mGutsMenuItem;
- protected NotificationPresenter mPresenter;
- protected NotificationEntryManager mEntryManager;
+ private NotificationPresenter mPresenter;
+ private NotificationSafeToRemoveCallback mNotificationLifetimeFinishedCallback;
private NotificationListContainer mListContainer;
private NotificationInfo.CheckSaveListener mCheckSaveListener;
private OnSettingsClickListener mOnSettingsClickListener;
- private String mKeyToRemoveOnGutsClosed;
+ @VisibleForTesting
+ protected String mKeyToRemoveOnGutsClosed;
public NotificationGutsManager(Context context) {
mContext = context;
@@ -91,24 +92,15 @@
}
public void setUpWithPresenter(NotificationPresenter presenter,
- NotificationEntryManager entryManager, NotificationListContainer listContainer,
+ NotificationListContainer listContainer,
NotificationInfo.CheckSaveListener checkSaveListener,
OnSettingsClickListener onSettingsClickListener) {
mPresenter = presenter;
- mEntryManager = entryManager;
mListContainer = listContainer;
mCheckSaveListener = checkSaveListener;
mOnSettingsClickListener = onSettingsClickListener;
}
- public String getKeyToRemoveOnGutsClosed() {
- return mKeyToRemoveOnGutsClosed;
- }
-
- public void setKeyToRemoveOnGutsClosed(String keyToRemoveOnGutsClosed) {
- mKeyToRemoveOnGutsClosed = keyToRemoveOnGutsClosed;
- }
-
public void onDensityOrFontScaleChanged(ExpandableNotificationRow row) {
setExposedGuts(row.getGuts());
bindGuts(row);
@@ -171,7 +163,9 @@
String key = sbn.getKey();
if (key.equals(mKeyToRemoveOnGutsClosed)) {
mKeyToRemoveOnGutsClosed = null;
- mEntryManager.removeNotification(key, mEntryManager.getLatestRankingMap());
+ if (mNotificationLifetimeFinishedCallback != null) {
+ mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
+ }
}
});
@@ -410,6 +404,37 @@
}
@Override
+ public void setCallback(NotificationSafeToRemoveCallback callback) {
+ mNotificationLifetimeFinishedCallback = callback;
+ }
+
+ @Override
+ public boolean shouldExtendLifetime(NotificationData.Entry entry) {
+ return entry != null
+ &&(mNotificationGutsExposed != null
+ && entry.row.getGuts() != null
+ && mNotificationGutsExposed == entry.row.getGuts()
+ && !mNotificationGutsExposed.isLeavebehind());
+ }
+
+ @Override
+ public void setShouldExtendLifetime(NotificationData.Entry entry, boolean shouldExtend) {
+ if (shouldExtend) {
+ mKeyToRemoveOnGutsClosed = entry.key;
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Keeping notification because it's showing guts. " + entry.key);
+ }
+ } else {
+ if (mKeyToRemoveOnGutsClosed != null && mKeyToRemoveOnGutsClosed.equals(entry.key)) {
+ mKeyToRemoveOnGutsClosed = null;
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Notification that was kept for guts was updated. " + entry.key);
+ }
+ }
+ }
+ }
+
+ @Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("NotificationGutsManager state:");
pw.print(" mKeyToRemoveOnGutsClosed: ");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index 6150c2f..4a05989 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -258,18 +258,6 @@
return mTrackingHeadsUp;
}
- /**
- * React to the removal of the notification in the heads up.
- *
- * @return true if the notification was removed and false if it still needs to be kept around
- * for a bit since it wasn't shown long enough
- */
- @Override
- public boolean removeNotification(@NonNull String key, boolean releaseImmediately) {
- return super.removeNotification(key, canRemoveImmediately(key)
- || releaseImmediately);
- }
-
@Override
public void snooze() {
super.snooze();
@@ -405,7 +393,8 @@
return (HeadsUpEntryPhone) getTopHeadsUpEntry();
}
- private boolean canRemoveImmediately(@NonNull String key) {
+ @Override
+ protected boolean canRemoveImmediately(@NonNull String key) {
if (mSwipedOutKeys.contains(key)) {
// We always instantly dismiss views being manually swiped out.
mSwipedOutKeys.remove(key);
@@ -414,7 +403,8 @@
HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key);
HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone();
- return headsUpEntry != topEntry || headsUpEntry.wasShownLongEnough();
+
+ return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 57e01e7..a900c14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -596,9 +596,10 @@
mContext.getString(R.string.instant_apps));
mCurrentNotifs.add(new Pair<>(pkg, userId));
String message = mContext.getString(R.string.instant_apps_message);
- PendingIntent appInfoAction = PendingIntent.getActivity(mContext, 0,
+ UserHandle user = UserHandle.of(userId);
+ PendingIntent appInfoAction = PendingIntent.getActivityAsUser(mContext, 0,
new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- .setData(Uri.fromParts("package", pkg, null)), 0);
+ .setData(Uri.fromParts("package", pkg, null)), 0, null, user);
Action action = new Notification.Action.Builder(null, mContext.getString(R.string.app_info),
appInfoAction).build();
@@ -611,8 +612,8 @@
.addFlags(Intent.FLAG_IGNORE_EPHEMERAL)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- PendingIntent pendingIntent = PendingIntent.getActivity(mContext,
- 0 /* requestCode */, browserIntent, 0 /* flags */);
+ PendingIntent pendingIntent = PendingIntent.getActivityAsUser(mContext,
+ 0 /* requestCode */, browserIntent, 0 /* flags */, null, user);
ComponentName aiaComponent = null;
try {
aiaComponent = AppGlobals.getPackageManager().getInstantAppInstallerComponent();
@@ -629,7 +630,8 @@
.putExtra(Intent.EXTRA_LONG_VERSION_CODE, appInfo.versionCode)
.putExtra(Intent.EXTRA_INSTANT_APP_FAILURE, pendingIntent);
- PendingIntent webPendingIntent = PendingIntent.getActivity(mContext, 0, goToWebIntent, 0);
+ PendingIntent webPendingIntent = PendingIntent.getActivityAsUser(mContext, 0,
+ goToWebIntent, 0, null, user);
Action webAction = new Notification.Action.Builder(null, mContext.getString(R.string.go_to_web),
webPendingIntent).build();
builder.addAction(webAction);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 22a727c..9beaa10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -800,7 +800,7 @@
this,
mNotificationPanel,
notifListContainer);
- mGutsManager.setUpWithPresenter(this, mEntryManager, notifListContainer, mCheckSaveListener,
+ mGutsManager.setUpWithPresenter(this, notifListContainer, mCheckSaveListener,
key -> {
try {
mBarService.onNotificationSettingsViewed(key);
@@ -1811,7 +1811,7 @@
mStatusBarWindowController.setHeadsUpShowing(false);
mHeadsUpManager.setHeadsUpGoingAway(false);
}
- mRemoteInputManager.removeRemoteInputEntriesKeptUntilCollapsed();
+ mRemoteInputManager.onPanelCollapsed();
});
}
}
@@ -1828,7 +1828,7 @@
@Override
public void onHeadsUpStateChanged(Entry entry, boolean isHeadsUp) {
- mEntryManager.onHeadsUpStateChanged(entry, isHeadsUp);
+ mEntryManager.updateNotificationRanking(null /* rankingMap */);
if (isHeadsUp) {
mDozeServiceHost.fireNotificationHeadsUp();
@@ -1858,7 +1858,7 @@
}
if (!isExpanded) {
- mRemoteInputManager.removeRemoteInputEntriesKeptUntilCollapsed();
+ mRemoteInputManager.onPanelCollapsed();
}
}
@@ -3751,7 +3751,7 @@
clearNotificationEffects();
}
if (newState == StatusBarState.KEYGUARD) {
- mRemoteInputManager.removeRemoteInputEntriesKeptUntilCollapsed();
+ mRemoteInputManager.onPanelCollapsed();
maybeEscalateHeadsUp();
}
}
@@ -4862,7 +4862,8 @@
removeNotification(parentToCancelFinal);
}
if (shouldAutoCancel(sbn)
- || mEntryManager.isNotificationKeptForRemoteInput(notificationKey)) {
+ || mRemoteInputManager.isNotificationKeptForRemoteInputHistory(
+ notificationKey)) {
// Automatically remove all notifications that we may have kept around longer
removeNotification(sbn);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java
index f04a115..32c972c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java
@@ -91,7 +91,7 @@
return new TestableAlertingNotificationManager();
}
- private StatusBarNotification createNewNotification(int id) {
+ protected StatusBarNotification createNewNotification(int id) {
Notification.Builder n = new Notification.Builder(mContext, "")
.setSmallIcon(R.drawable.ic_person)
.setContentTitle("Title")
@@ -154,7 +154,7 @@
public void testRemoveNotification_forceRemove() {
mAlertingNotificationManager.showNotification(mEntry);
- //Remove forcibly with releaseImmediately = true.
+ // Remove forcibly with releaseImmediately = true.
mAlertingNotificationManager.removeNotification(mEntry.key, true /* releaseImmediately */);
assertFalse(mAlertingNotificationManager.contains(mEntry.key));
@@ -173,4 +173,30 @@
assertEquals(0, mAlertingNotificationManager.getAllEntries().count());
}
+
+ @Test
+ public void testShouldExtendLifetime_notShownLongEnough() {
+ mAlertingNotificationManager.showNotification(mEntry);
+
+ // The entry has just been added so the lifetime should be extended
+ assertTrue(mAlertingNotificationManager.shouldExtendLifetime(mEntry));
+ }
+
+ @Test
+ public void testSetShouldExtendLifetime_setShouldExtend() {
+ mAlertingNotificationManager.showNotification(mEntry);
+
+ mAlertingNotificationManager.setShouldExtendLifetime(mEntry, true /* shouldExtend */);
+
+ assertTrue(mAlertingNotificationManager.mExtendedLifetimeAlertEntries.contains(mEntry));
+ }
+
+ @Test
+ public void testSetShouldExtendLifetime_setShouldNotExtend() {
+ mAlertingNotificationManager.mExtendedLifetimeAlertEntries.add(mEntry);
+
+ mAlertingNotificationManager.setShouldExtendLifetime(mEntry, false /* shouldExtend */);
+
+ assertFalse(mAlertingNotificationManager.mExtendedLifetimeAlertEntries.contains(mEntry));
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
index 8129b01..09c1931 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
@@ -86,8 +86,8 @@
entryManager.setUpWithPresenter(mPresenter, mListContainer, mEntryManagerCallback,
mHeadsUpManager);
- gutsManager.setUpWithPresenter(mPresenter, entryManager, mListContainer,
- mCheckSaveListener, mOnClickListener);
+ gutsManager.setUpWithPresenter(mPresenter, mListContainer, mCheckSaveListener,
+ mOnClickListener);
notificationLogger.setUpWithEntryManager(entryManager, mListContainer);
mediaManager.setUpWithPresenter(mPresenter, entryManager);
remoteInputManager.setUpWithPresenter(mPresenter, entryManager, mRemoteInputManagerCallback,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
index 3cafaf4..7b0c0a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
@@ -85,13 +85,6 @@
}
@Test
- public void testPostNotificationRemovesKeyKeptForRemoteInput() {
- mListener.onNotificationPosted(mSbn, mRanking);
- TestableLooper.get(this).processAllMessages();
- verify(mEntryManager).removeKeyKeptForRemoteInput(mSbn.getKey());
- }
-
- @Test
public void testNotificationUpdateCallsUpdateNotification() {
when(mNotificationData.get(mSbn.getKey())).thenReturn(new NotificationData.Entry(mSbn));
mListener.onNotificationPosted(mSbn, mRanking);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
index afe2cf6..b2493b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
@@ -1,8 +1,10 @@
package com.android.systemui.statusbar;
-import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -10,6 +12,7 @@
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
@@ -22,8 +25,14 @@
import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputActiveExtender;
+import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputHistoryExtender;
+import com.android.systemui.statusbar.NotificationRemoteInputManager.SmartReplyHistoryExtender;
+
import com.google.android.collect.Sets;
+import junit.framework.Assert;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -41,6 +50,7 @@
@Mock private RemoteInputController.Delegate mDelegate;
@Mock private NotificationRemoteInputManager.Callback mCallback;
@Mock private RemoteInputController mController;
+ @Mock private SmartReplyController mSmartReplyController;
@Mock private NotificationListenerService.RankingMap mRanking;
@Mock private ExpandableNotificationRow mRow;
@@ -51,6 +61,9 @@
private TestableNotificationRemoteInputManager mRemoteInputManager;
private StatusBarNotification mSbn;
private NotificationData.Entry mEntry;
+ private RemoteInputHistoryExtender mRemoteInputHistoryExtender;
+ private SmartReplyHistoryExtender mSmartReplyHistoryExtender;
+ private RemoteInputActiveExtender mRemoteInputActiveExtender;
@Before
public void setUp() {
@@ -58,9 +71,9 @@
mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager);
mDependency.injectTestDependency(NotificationLockscreenUserManager.class,
mLockscreenUserManager);
+ mDependency.injectTestDependency(SmartReplyController.class, mSmartReplyController);
when(mPresenter.getHandler()).thenReturn(Handler.createAsync(Looper.myLooper()));
- when(mEntryManager.getLatestRankingMap()).thenReturn(mRanking);
mRemoteInputManager = new TestableNotificationRemoteInputManager(mContext);
mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID,
@@ -70,20 +83,10 @@
mRemoteInputManager.setUpWithPresenterForTest(mPresenter, mEntryManager, mCallback,
mDelegate, mController);
- }
-
- @Test
- public void testOnRemoveNotificationNotKept() {
- assertFalse(mRemoteInputManager.onRemoveNotification(mEntry));
- assertTrue(mRemoteInputManager.getRemoteInputEntriesToRemoveOnCollapse().isEmpty());
- }
-
- @Test
- public void testOnRemoveNotificationKept() {
- when(mController.isRemoteInputActive(mEntry)).thenReturn(true);
- assertTrue(mRemoteInputManager.onRemoveNotification(mEntry));
- assertTrue(mRemoteInputManager.getRemoteInputEntriesToRemoveOnCollapse().equals(
- Sets.newArraySet(mEntry)));
+ for (NotificationLifetimeExtender extender : mRemoteInputManager.getLifetimeExtenders()) {
+ extender.setCallback(
+ mock(NotificationLifetimeExtender.NotificationSafeToRemoveCallback.class));
+ }
}
@Test
@@ -95,15 +98,104 @@
}
@Test
- public void testRemoveRemoteInputEntriesKeptUntilCollapsed() {
- mRemoteInputManager.getRemoteInputEntriesToRemoveOnCollapse().add(mEntry);
- mRemoteInputManager.removeRemoteInputEntriesKeptUntilCollapsed();
+ public void testShouldExtendLifetime_remoteInputActive() {
+ when(mController.isRemoteInputActive(mEntry)).thenReturn(true);
- assertTrue(mRemoteInputManager.getRemoteInputEntriesToRemoveOnCollapse().isEmpty());
- verify(mController).removeRemoteInput(mEntry, null);
- verify(mEntryManager).removeNotification(mEntry.key, mRanking);
+ assertTrue(mRemoteInputActiveExtender.shouldExtendLifetime(mEntry));
}
+ @Test
+ public void testShouldExtendLifetime_isSpinning() {
+ NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY = true;
+ when(mController.isSpinning(mEntry.key)).thenReturn(true);
+
+ assertTrue(mRemoteInputHistoryExtender.shouldExtendLifetime(mEntry));
+ }
+
+ @Test
+ public void testShouldExtendLifetime_recentRemoteInput() {
+ NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY = true;
+ mEntry.lastRemoteInputSent = SystemClock.elapsedRealtime();
+
+ assertTrue(mRemoteInputHistoryExtender.shouldExtendLifetime(mEntry));
+ }
+
+ @Test
+ public void testShouldExtendLifetime_smartReplySending() {
+ NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY = true;
+ when(mSmartReplyController.isSendingSmartReply(mEntry.key)).thenReturn(true);
+
+ assertTrue(mSmartReplyHistoryExtender.shouldExtendLifetime(mEntry));
+ }
+
+ @Test
+ public void testNotificationWithRemoteInputActiveIsRemovedOnCollapse() {
+ mRemoteInputActiveExtender.setShouldExtendLifetime(mEntry, true);
+
+ assertEquals(mRemoteInputManager.getEntriesKeptForRemoteInputActive(),
+ Sets.newArraySet(mEntry));
+
+ mRemoteInputManager.onPanelCollapsed();
+
+ assertTrue(mRemoteInputManager.getEntriesKeptForRemoteInputActive().isEmpty());
+ }
+
+ @Test
+ public void testRebuildWithRemoteInput_noExistingInputNoSpinner() {
+ StatusBarNotification newSbn =
+ mRemoteInputManager.rebuildNotificationWithRemoteInput(mEntry, "A Reply", false);
+ CharSequence[] messages = newSbn.getNotification().extras
+ .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
+ assertEquals(1, messages.length);
+ assertEquals("A Reply", messages[0]);
+ assertFalse(newSbn.getNotification().extras
+ .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
+ assertTrue(newSbn.getNotification().extras
+ .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
+ }
+
+ @Test
+ public void testRebuildWithRemoteInput_noExistingInputWithSpinner() {
+ StatusBarNotification newSbn =
+ mRemoteInputManager.rebuildNotificationWithRemoteInput(mEntry, "A Reply", true);
+ CharSequence[] messages = newSbn.getNotification().extras
+ .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
+ assertEquals(1, messages.length);
+ assertEquals("A Reply", messages[0]);
+ assertTrue(newSbn.getNotification().extras
+ .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
+ assertTrue(newSbn.getNotification().extras
+ .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
+ }
+
+ @Test
+ public void testRebuildWithRemoteInput_withExistingInput() {
+ // Setup a notification entry with 1 remote input.
+ StatusBarNotification newSbn =
+ mRemoteInputManager.rebuildNotificationWithRemoteInput(mEntry, "A Reply", false);
+ NotificationData.Entry entry = new NotificationData.Entry(newSbn);
+
+ // Try rebuilding to add another reply.
+ newSbn = mRemoteInputManager.rebuildNotificationWithRemoteInput(entry, "Reply 2", true);
+ CharSequence[] messages = newSbn.getNotification().extras
+ .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
+ assertEquals(2, messages.length);
+ assertEquals("Reply 2", messages[0]);
+ assertEquals("A Reply", messages[1]);
+ }
+
+ @Test
+ public void testRebuildNotificationForCanceledSmartReplies() {
+ // Try rebuilding to remove spinner and hide buttons.
+ StatusBarNotification newSbn =
+ mRemoteInputManager.rebuildNotificationForCanceledSmartReplies(mEntry);
+ assertFalse(newSbn.getNotification().extras
+ .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
+ assertTrue(newSbn.getNotification().extras
+ .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
+ }
+
+
private class TestableNotificationRemoteInputManager extends NotificationRemoteInputManager {
public TestableNotificationRemoteInputManager(Context context) {
@@ -118,5 +210,15 @@
super.setUpWithPresenter(presenter, entryManager, callback, delegate);
mRemoteInputController = controller;
}
+
+ @Override
+ protected void addLifetimeExtenders() {
+ mRemoteInputActiveExtender = new RemoteInputActiveExtender();
+ mRemoteInputHistoryExtender = new RemoteInputHistoryExtender();
+ mSmartReplyHistoryExtender = new SmartReplyHistoryExtender();
+ mLifetimeExtenders.add(mRemoteInputHistoryExtender);
+ mLifetimeExtenders.add(mSmartReplyHistoryExtender);
+ mLifetimeExtenders.add(mRemoteInputActiveExtender);
+ }
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
index ada5785..17daaac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SmartReplyControllerTest.java
@@ -23,8 +23,11 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.app.Notification;
import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.support.test.filters.SmallTest;
import android.testing.AndroidTestingRunner;
@@ -35,6 +38,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import org.junit.Before;
import org.junit.Test;
@@ -46,97 +50,88 @@
@TestableLooper.RunWithLooper
@SmallTest
public class SmartReplyControllerTest extends SysuiTestCase {
- private static final String TEST_NOTIFICATION_KEY = "akey";
+ private static final String TEST_PACKAGE_NAME = "test";
+ private static final int TEST_UID = 0;
private static final String TEST_CHOICE_TEXT = "A Reply";
private static final int TEST_CHOICE_INDEX = 2;
private static final int TEST_CHOICE_COUNT = 4;
private Notification mNotification;
private NotificationData.Entry mEntry;
+ private SmartReplyController mSmartReplyController;
+ private NotificationRemoteInputManager mRemoteInputManager;
- @Mock
- private NotificationEntryManager mNotificationEntryManager;
- @Mock
- private IStatusBarService mIStatusBarService;
+ @Mock private NotificationPresenter mPresenter;
+ @Mock private RemoteInputController.Delegate mDelegate;
+ @Mock private NotificationRemoteInputManager.Callback mCallback;
+ @Mock private StatusBarNotification mSbn;
+ @Mock private NotificationEntryManager mNotificationEntryManager;
+ @Mock private IStatusBarService mIStatusBarService;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
-
mDependency.injectTestDependency(NotificationEntryManager.class,
mNotificationEntryManager);
mDependency.injectTestDependency(IStatusBarService.class, mIStatusBarService);
+ mSmartReplyController = new SmartReplyController();
+ mDependency.injectTestDependency(SmartReplyController.class,
+ mSmartReplyController);
+
+ mRemoteInputManager = new NotificationRemoteInputManager(mContext);
+ mRemoteInputManager.setUpWithPresenter(mPresenter, mNotificationEntryManager, mCallback,
+ mDelegate);
mNotification = new Notification.Builder(mContext, "")
.setSmallIcon(R.drawable.ic_person)
.setContentTitle("Title")
.setContentText("Text").build();
- StatusBarNotification sbn = mock(StatusBarNotification.class);
- when(sbn.getNotification()).thenReturn(mNotification);
- when(sbn.getKey()).thenReturn(TEST_NOTIFICATION_KEY);
- mEntry = new NotificationData.Entry(sbn);
+
+ mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID,
+ 0, mNotification, new UserHandle(ActivityManager.getCurrentUser()), null, 0);
+ mEntry = new NotificationData.Entry(mSbn);
}
@Test
public void testSendSmartReply_updatesRemoteInput() {
- StatusBarNotification sbn = mock(StatusBarNotification.class);
- when(sbn.getKey()).thenReturn(TEST_NOTIFICATION_KEY);
- when(mNotificationEntryManager.rebuildNotificationWithRemoteInput(
- argThat(entry -> entry.notification.getKey().equals(TEST_NOTIFICATION_KEY)),
- eq(TEST_CHOICE_TEXT), eq(true))).thenReturn(sbn);
-
- SmartReplyController controller = new SmartReplyController();
- controller.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT);
+ mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT);
// Sending smart reply should make calls to NotificationEntryManager
// to update the notification with reply and spinner.
- verify(mNotificationEntryManager).rebuildNotificationWithRemoteInput(
- argThat(entry -> entry.notification.getKey().equals(TEST_NOTIFICATION_KEY)),
- eq(TEST_CHOICE_TEXT), eq(true));
verify(mNotificationEntryManager).updateNotification(
- argThat(sbn2 -> sbn2.getKey().equals(TEST_NOTIFICATION_KEY)), isNull());
+ argThat(sbn -> sbn.getKey().equals(mSbn.getKey())), isNull());
}
@Test
public void testSendSmartReply_logsToStatusBar() throws RemoteException {
- StatusBarNotification sbn = mock(StatusBarNotification.class);
- when(sbn.getKey()).thenReturn(TEST_NOTIFICATION_KEY);
- when(mNotificationEntryManager.rebuildNotificationWithRemoteInput(
- argThat(entry -> entry.notification.getKey().equals(TEST_NOTIFICATION_KEY)),
- eq(TEST_CHOICE_TEXT), eq(true))).thenReturn(sbn);
-
- SmartReplyController controller = new SmartReplyController();
- controller.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT);
+ mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT);
// Check we log the result to the status bar service.
- verify(mIStatusBarService).onNotificationSmartReplySent(TEST_NOTIFICATION_KEY,
+ verify(mIStatusBarService).onNotificationSmartReplySent(mSbn.getKey(),
TEST_CHOICE_INDEX);
}
@Test
public void testShowSmartReply_logsToStatusBar() throws RemoteException {
- SmartReplyController controller = new SmartReplyController();
- controller.smartRepliesAdded(mEntry, TEST_CHOICE_COUNT);
+ mSmartReplyController.smartRepliesAdded(mEntry, TEST_CHOICE_COUNT);
// Check we log the result to the status bar service.
- verify(mIStatusBarService).onNotificationSmartRepliesAdded(TEST_NOTIFICATION_KEY,
+ verify(mIStatusBarService).onNotificationSmartRepliesAdded(mSbn.getKey(),
TEST_CHOICE_COUNT);
}
@Test
public void testSendSmartReply_reportsSending() {
- SmartReplyController controller = new SmartReplyController();
- controller.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT);
+ mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT);
- assertTrue(controller.isSendingSmartReply(TEST_NOTIFICATION_KEY));
+ assertTrue(mSmartReplyController.isSendingSmartReply(mSbn.getKey()));
}
@Test
public void testSendingSmartReply_afterRemove_shouldReturnFalse() {
- SmartReplyController controller = new SmartReplyController();
- controller.isSendingSmartReply(TEST_NOTIFICATION_KEY);
- controller.stopSending(mEntry);
+ mSmartReplyController.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT);
+ mSmartReplyController.stopSending(mEntry);
- assertFalse(controller.isSendingSmartReply(TEST_NOTIFICATION_KEY));
+ assertFalse(mSmartReplyController.isSendingSmartReply(mSbn.getKey()));
}
}
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 6543bdb..dacf59c 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
@@ -57,6 +57,7 @@
import com.android.systemui.ForegroundServiceController;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.NotificationLifetimeExtender;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationMediaManager;
@@ -84,7 +85,6 @@
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -143,6 +143,10 @@
public CountDownLatch getCountDownLatch() {
return mCountDownLatch;
}
+
+ public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
+ return mNotificationLifetimeExtenders;
+ }
}
private void setUserSentiment(String key, int sentiment) {
@@ -279,7 +283,6 @@
verify(mBarService, never()).onNotificationError(any(), any(), anyInt(), anyInt(), anyInt(),
any(), anyInt());
- verify(mRemoteInputManager).onUpdateNotification(mEntry);
verify(mPresenter).updateNotificationViews();
verify(mForegroundServiceController).updateNotification(eq(mSbn), anyInt());
verify(mCallback).onNotificationUpdated(mSbn);
@@ -301,8 +304,6 @@
any(), anyInt());
verify(mMediaManager).onNotificationRemoved(mSbn.getKey());
- verify(mRemoteInputManager).onRemoveNotification(mEntry);
- verify(mSmartReplyController).stopSending(mEntry);
verify(mForegroundServiceController).removeNotification(mSbn);
verify(mListContainer).cleanUpViewState(mRow);
verify(mPresenter).updateNotificationViews();
@@ -313,17 +314,23 @@
}
@Test
- public void testRemoveNotification_blockedBySendingSmartReply() throws Exception {
+ public void testRemoveNotification_blockedByLifetimeExtender() {
com.android.systemui.util.Assert.isNotMainThread();
+ NotificationLifetimeExtender extender = mock(NotificationLifetimeExtender.class);
+ when(extender.shouldExtendLifetime(mEntry)).thenReturn(true);
+
+ ArrayList<NotificationLifetimeExtender> extenders = mEntryManager.getLifetimeExtenders();
+ extenders.clear();
+ extenders.add(extender);
+
mEntry.row = mRow;
mEntryManager.getNotificationData().add(mEntry);
- when(mSmartReplyController.isSendingSmartReply(mEntry.key)).thenReturn(true);
mEntryManager.removeNotification(mSbn.getKey(), mRankingMap);
assertNotNull(mEntryManager.getNotificationData().get(mSbn.getKey()));
- assertTrue(mEntryManager.isNotificationKeptForRemoteInput(mEntry.key));
+ verify(extender).setShouldExtendLifetime(mEntry, true);
}
@Test
@@ -411,61 +418,6 @@
}
@Test
- public void testRebuildWithRemoteInput_noExistingInputNoSpinner() {
- StatusBarNotification newSbn =
- mEntryManager.rebuildNotificationWithRemoteInput(mEntry, "A Reply", false);
- CharSequence[] messages = newSbn.getNotification().extras
- .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
- Assert.assertEquals(1, messages.length);
- Assert.assertEquals("A Reply", messages[0]);
- Assert.assertFalse(newSbn.getNotification().extras
- .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
- Assert.assertTrue(newSbn.getNotification().extras
- .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
- }
-
- @Test
- public void testRebuildWithRemoteInput_noExistingInputWithSpinner() {
- StatusBarNotification newSbn =
- mEntryManager.rebuildNotificationWithRemoteInput(mEntry, "A Reply", true);
- CharSequence[] messages = newSbn.getNotification().extras
- .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
- Assert.assertEquals(1, messages.length);
- Assert.assertEquals("A Reply", messages[0]);
- Assert.assertTrue(newSbn.getNotification().extras
- .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
- Assert.assertTrue(newSbn.getNotification().extras
- .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
- }
-
- @Test
- public void testRebuildWithRemoteInput_withExistingInput() {
- // Setup a notification entry with 1 remote input.
- StatusBarNotification newSbn =
- mEntryManager.rebuildNotificationWithRemoteInput(mEntry, "A Reply", false);
- NotificationData.Entry entry = new NotificationData.Entry(newSbn);
-
- // Try rebuilding to add another reply.
- newSbn = mEntryManager.rebuildNotificationWithRemoteInput(entry, "Reply 2", true);
- CharSequence[] messages = newSbn.getNotification().extras
- .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
- Assert.assertEquals(2, messages.length);
- Assert.assertEquals("Reply 2", messages[0]);
- Assert.assertEquals("A Reply", messages[1]);
- }
-
- @Test
- public void testRebuildNotificationForCanceledSmartReplies() {
- // Try rebuilding to remove spinner and hide buttons.
- StatusBarNotification newSbn =
- mEntryManager.rebuildNotificationForCanceledSmartReplies(mEntry);
- Assert.assertFalse(newSbn.getNotification().extras
- .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
- Assert.assertTrue(newSbn.getNotification().extras
- .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
- }
-
- @Test
public void testUpdateNotificationRanking() {
when(mPresenter.isDeviceProvisioned()).thenReturn(true);
when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
index 676cb61..6656fdd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
@@ -22,6 +22,8 @@
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@@ -30,6 +32,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.spy;
@@ -54,6 +57,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
+import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationTestHelper;
@@ -99,7 +103,7 @@
mHelper = new NotificationTestHelper(mContext);
mGutsManager = new NotificationGutsManager(mContext);
- mGutsManager.setUpWithPresenter(mPresenter, mEntryManager, mStackScroller,
+ mGutsManager.setUpWithPresenter(mPresenter, mStackScroller,
mCheckSaveListener, mOnSettingsClickListener);
}
@@ -346,6 +350,35 @@
eq(true) /* isUserSentimentNegative */);
}
+ @Test
+ public void testShouldExtendLifetime() {
+ NotificationGuts guts = new NotificationGuts(mContext);
+ ExpandableNotificationRow row = spy(createTestNotificationRow());
+ doReturn(guts).when(row).getGuts();
+ NotificationData.Entry entry = row.getEntry();
+ entry.row = row;
+ mGutsManager.setExposedGuts(guts);
+
+ assertTrue(mGutsManager.shouldExtendLifetime(entry));
+ }
+
+ @Test
+ public void testSetShouldExtendLifetime_setShouldExtend() {
+ NotificationData.Entry entry = createTestNotificationRow().getEntry();
+ mGutsManager.setShouldExtendLifetime(entry, true /* shouldExtend */);
+
+ assertTrue(entry.key.equals(mGutsManager.mKeyToRemoveOnGutsClosed));
+ }
+
+ @Test
+ public void testSetShouldExtendLifetime_setShouldNotExtend() {
+ NotificationData.Entry entry = createTestNotificationRow().getEntry();
+ mGutsManager.mKeyToRemoveOnGutsClosed = entry.key;
+ mGutsManager.setShouldExtendLifetime(entry, false /* shouldExtend */);
+
+ assertNull(mGutsManager.mKeyToRemoveOnGutsClosed);
+ }
+
////////////////////////////////////////////////////////////////////////////////////////////////
// Utility methods:
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index c19188c..ce0bd58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -112,7 +112,8 @@
mEntryManager = new TestableNotificationEntryManager(mSystemServicesProxy, mPowerManager,
mContext);
- mEntryManager.setUpForTest(mock(NotificationPresenter.class), null, null, null, mNotificationData);
+ mEntryManager.setUpForTest(mock(NotificationPresenter.class), null, null, mHeadsUpManager,
+ mNotificationData);
mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager);
NotificationShelf notificationShelf = spy(new NotificationShelf(getContext(), null));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
index bdf7cd3..a81d17f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
@@ -23,6 +23,7 @@
import com.android.systemui.statusbar.AlertingNotificationManager;
import com.android.systemui.statusbar.AlertingNotificationManagerTest;
+import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
import org.junit.Before;
@@ -83,4 +84,26 @@
assertFalse(mHeadsUpManager.contains(mEntry.key));
}
+
+ @Test
+ public void testShouldExtendLifetime_swipedOut() {
+ mHeadsUpManager.showNotification(mEntry);
+ mHeadsUpManager.addSwipedOutNotification(mEntry.key);
+
+ // Notification is swiped so its lifetime should not be extended even if it hasn't been
+ // shown long enough
+ assertFalse(mHeadsUpManager.shouldExtendLifetime(mEntry));
+ }
+
+ @Test
+ public void testShouldExtendLifetime_notTopEntry() {
+ NotificationData.Entry laterEntry = new NotificationData.Entry(createNewNotification(1));
+ laterEntry.row = mRow;
+ mHeadsUpManager.showNotification(mEntry);
+ mHeadsUpManager.showNotification(laterEntry);
+
+ // Notification is "behind" a higher priority notification so we have no reason to keep
+ // its lifetime extended
+ assertFalse(mHeadsUpManager.shouldExtendLifetime(mEntry));
+ }
}
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 566ce4f..c44a81e 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -50,6 +50,7 @@
import android.telephony.VoLteServiceState;
import android.util.LocalLog;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
import com.android.internal.telephony.IOnSubscriptionsChangedListener;
import com.android.internal.telephony.IPhoneStateListener;
@@ -82,7 +83,8 @@
* Eventually we may want to remove the notion of dummy value but for now this
* looks like the best approach.
*/
-class TelephonyRegistry extends ITelephonyRegistry.Stub {
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public class TelephonyRegistry extends ITelephonyRegistry.Stub {
private static final String TAG = "TelephonyRegistry";
private static final boolean DBG = false; // STOPSHIP if true
private static final boolean DBG_LOC = false; // STOPSHIP if true
@@ -315,7 +317,8 @@
// calls go through a oneway interface and local calls going through a
// handler before they get to app code.
- TelephonyRegistry(Context context) {
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public TelephonyRegistry(Context context) {
CellLocation location = CellLocation.getEmpty();
mContext = context;
diff --git a/services/core/java/com/android/server/am/OWNERS b/services/core/java/com/android/server/am/OWNERS
index f60c5c3..79c98e5 100644
--- a/services/core/java/com/android/server/am/OWNERS
+++ b/services/core/java/com/android/server/am/OWNERS
@@ -4,7 +4,6 @@
jsharkey@google.com
hackbod@google.com
omakoto@google.com
-fkupolov@google.com
ctate@google.com
huiyu@google.com
mwachens@google.com
@@ -28,7 +27,4 @@
michaelwr@google.com
narayan@google.com
-per-file GlobalSettingsToPropertiesMapper.java=fkupolov@google.com
-per-file GlobalSettingsToPropertiesMapper.java=omakoto@google.com
-per-file GlobalSettingsToPropertiesMapper.java=svetoslavganov@google.com
-per-file GlobalSettingsToPropertiesMapper.java=yamasani@google.com
+per-file GlobalSettingsToPropertiesMapper.java = omakoto@google.com, svetoslavganov@google.com, yamasani@google.com
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 9b097bf..07f3e17 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -1988,6 +1988,14 @@
mRequiredVerifierPackage, null /*finishedReceiver*/,
updateUserIds, instantUserIds);
}
+ // If package installer is defined, notify package installer about new
+ // app installed
+ if (mRequiredInstallerPackage != null) {
+ sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
+ extras, Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND /*flags*/,
+ mRequiredInstallerPackage, null /*finishedReceiver*/,
+ firstUserIds, instantUserIds);
+ }
// Send replaced for users that don't see the package for the first time
if (update) {
diff --git a/telephony/java/android/telephony/PhoneStateListener.java b/telephony/java/android/telephony/PhoneStateListener.java
index bd6a59d..498be96 100644
--- a/telephony/java/android/telephony/PhoneStateListener.java
+++ b/telephony/java/android/telephony/PhoneStateListener.java
@@ -23,6 +23,7 @@
import android.os.Looper;
import android.os.Message;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.IPhoneStateListener;
import java.lang.ref.WeakReference;
@@ -778,8 +779,12 @@
}
}
+ /**
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@UnsupportedAppUsage
- IPhoneStateListener callback = new IPhoneStateListenerStub(this);
+ public final IPhoneStateListener callback = new IPhoneStateListenerStub(this);
private void log(String s) {
Rlog.d(LOG_TAG, s);