diff options
Diffstat (limited to 'packages/SystemUI/src')
85 files changed, 3680 insertions, 2320 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java index e440731dcd47..3b5f34cd84cc 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java @@ -16,13 +16,16 @@ package com.android.keyguard; +import android.app.ActivityManager; import android.app.AlarmManager; +import android.app.IActivityManager; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.os.Handler; import android.os.Looper; +import android.os.RemoteException; import android.os.UserHandle; import android.support.v4.graphics.ColorUtils; import android.text.TextUtils; @@ -51,9 +54,11 @@ public class KeyguardStatusView extends GridLayout { private final LockPatternUtils mLockPatternUtils; private final AlarmManager mAlarmManager; + private final IActivityManager mIActivityManager; private final float mSmallClockScale; private final float mWidgetPadding; + private TextView mLogoutView; private TextClock mClockView; private View mClockSeparator; private TextView mOwnerInfo; @@ -80,6 +85,7 @@ public class KeyguardStatusView extends GridLayout { if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing); refresh(); updateOwnerInfo(); + updateLogoutView(); } } @@ -97,6 +103,12 @@ public class KeyguardStatusView extends GridLayout { public void onUserSwitchComplete(int userId) { refresh(); updateOwnerInfo(); + updateLogoutView(); + } + + @Override + public void onLogoutEnabledChanged() { + updateLogoutView(); } }; @@ -111,6 +123,7 @@ public class KeyguardStatusView extends GridLayout { public KeyguardStatusView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + mIActivityManager = ActivityManager.getService(); mLockPatternUtils = new LockPatternUtils(getContext()); mHandler = new Handler(Looper.myLooper()); mSmallClockScale = getResources().getDimension(R.dimen.widget_small_font_size) @@ -145,6 +158,9 @@ public class KeyguardStatusView extends GridLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); + mLogoutView = findViewById(R.id.logout); + mLogoutView.setOnClickListener(this::onLogoutClicked); + mClockContainer = findViewById(R.id.keyguard_clock_container); mClockView = findViewById(R.id.clock_view); mClockView.setShowCurrentUserTime(true); @@ -164,6 +180,7 @@ public class KeyguardStatusView extends GridLayout { setEnableMarquee(shouldMarquee); refresh(); updateOwnerInfo(); + updateLogoutView(); // Disable elegant text height because our fancy colon makes the ymin value huge for no // reason. @@ -213,14 +230,28 @@ public class KeyguardStatusView extends GridLayout { } public int getClockBottom() { - return mKeyguardSlice.getVisibility() == VISIBLE ? mKeyguardSlice.getBottom() - : mClockView.getBottom(); + if (mOwnerInfo != null && mOwnerInfo.getVisibility() == VISIBLE) { + return mOwnerInfo.getBottom(); + } else { + return mClockContainer.getBottom(); + } + } + + public int getLogoutButtonHeight() { + return mLogoutView.getVisibility() == VISIBLE ? mLogoutView.getHeight() : 0; } public float getClockTextSize() { return mClockView.getTextSize(); } + private void updateLogoutView() { + mLogoutView.setVisibility(shouldShowLogout() ? VISIBLE : GONE); + // Logout button will stay in language of user 0 if we don't set that manually. + mLogoutView.setText(mContext.getResources().getString( + com.android.internal.R.string.global_action_logout)); + } + private void updateOwnerInfo() { if (mOwnerInfo == null) return; String ownerInfo = getOwnerInfo(); @@ -309,6 +340,7 @@ public class KeyguardStatusView extends GridLayout { mDarkAmount = darkAmount; boolean dark = darkAmount == 1; + mLogoutView.setAlpha(dark ? 0 : 1); final int N = mClockContainer.getChildCount(); for (int i = 0; i < N; i++) { View child = mClockContainer.getChildAt(i); @@ -340,4 +372,19 @@ public class KeyguardStatusView extends GridLayout { child.setAlpha(mDarkAmount == 1 && mPulsing ? 0.8f : 1); } } + + private boolean shouldShowLogout() { + return KeyguardUpdateMonitor.getInstance(mContext).isLogoutEnabled() + && KeyguardUpdateMonitor.getCurrentUser() != UserHandle.USER_SYSTEM; + } + + private void onLogoutClicked(View view) { + int currentUserId = KeyguardUpdateMonitor.getCurrentUser(); + try { + mIActivityManager.switchUser(UserHandle.USER_SYSTEM); + mIActivityManager.stopUser(currentUserId, true /*force*/, null); + } catch (RemoteException re) { + Log.e(TAG, "Failed to logout user", re); + } + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 9f5af0838b23..d7196bf68d36 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -141,6 +141,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener { private static final int MSG_USER_UNLOCKED = 334; private static final int MSG_ASSISTANT_STACK_CHANGED = 335; private static final int MSG_FINGERPRINT_AUTHENTICATION_CONTINUE = 336; + private static final int MSG_DEVICE_POLICY_MANAGER_STATE_CHANGED = 337; private static final int MSG_LOCALE_CHANGED = 500; /** Fingerprint state: Not listening to fingerprint. */ @@ -226,6 +227,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener { private LockPatternUtils mLockPatternUtils; private final IDreamManager mDreamManager; private boolean mIsDreaming; + private final DevicePolicyManager mDevicePolicyManager; + private boolean mLogoutEnabled; /** * Short delay before restarting fingerprint authentication after a successful try @@ -333,6 +336,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener { break; case MSG_LOCALE_CHANGED: handleLocaleChanged(); + break; + case MSG_DEVICE_POLICY_MANAGER_STATE_CHANGED: + updateLogoutEnabled(); break; } } @@ -801,6 +807,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener { mHandler.obtainMessage(MSG_SERVICE_STATE_CHANGE, subId, 0, serviceState)); } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { mHandler.sendEmptyMessage(MSG_LOCALE_CHANGED); + } else if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals( + action)) { + mHandler.sendEmptyMessage(MSG_DEVICE_POLICY_MANAGER_STATE_CHANGED); } } }; @@ -1166,6 +1175,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener { filter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED); filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); + filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); context.registerReceiver(mBroadcastReceiver, filter); final IntentFilter bootCompleteFilter = new IntentFilter(); @@ -1220,6 +1230,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener { ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); mUserManager = context.getSystemService(UserManager.class); + mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class); + mLogoutEnabled = mDevicePolicyManager.isLogoutEnabled(); } private void updateFingerprintListeningState() { @@ -1986,6 +1998,26 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener { return null; // not found } + /** + * @return a cached version of DevicePolicyManager.isLogoutEnabled() + */ + public boolean isLogoutEnabled() { + return mLogoutEnabled; + } + + private void updateLogoutEnabled() { + boolean logoutEnabled = mDevicePolicyManager.isLogoutEnabled(); + if (mLogoutEnabled != logoutEnabled) { + mLogoutEnabled = logoutEnabled; + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onLogoutEnabledChanged(); + } + } + } + } + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("KeyguardUpdateMonitor state:"); pw.println(" SIM States:"); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index 6284000ab99d..730ccb998364 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -291,4 +291,11 @@ public class KeyguardUpdateMonitorCallback { * @see KeyguardIndicationController#showTransientIndication(CharSequence) */ public void onTrustAgentErrorMessage(CharSequence message) { } + + + /** + * Called when a value of logout enabled is change. + */ + public void onLogoutEnabledChanged() { } + } diff --git a/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java index b7e1d67a5b3a..1185f45469df 100644 --- a/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java @@ -47,6 +47,8 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import static com.android.systemui.shared.system.NavigationBarCompat.InteractionType; + /** * Class to send information from overview to launcher with a binder. */ @@ -66,6 +68,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private IOverviewProxy mOverviewProxy; private int mConnectionBackoffAttempts; + private CharSequence mOnboardingText; + private @InteractionType int mInteractionFlags; private ISystemUiProxy mSysUiProxy = new ISystemUiProxy.Stub() { @@ -98,9 +102,27 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public void onRecentsAnimationStarted() { long token = Binder.clearCallingIdentity(); try { - mHandler.post(() -> { - notifyRecentsAnimationStarted(); - }); + mHandler.post(OverviewProxyService.this::notifyRecentsAnimationStarted); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + public void setRecentsOnboardingText(CharSequence text) { + mOnboardingText = text; + } + + public void setInteractionState(@InteractionType int flags) { + long token = Binder.clearCallingIdentity(); + try { + if (mInteractionFlags != flags) { + mInteractionFlags = flags; + mHandler.post(() -> { + for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { + mConnectionCallbacks.get(i).onInteractionFlagsChanged(flags); + } + }); + } } finally { Binder.restoreCallingIdentity(token); } @@ -223,8 +245,12 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis return mOverviewProxy; } - public ComponentName getLauncherComponent() { - return mLauncherComponentName; + public CharSequence getOnboardingText() { + return mOnboardingText; + } + + public int getInteractionFlags() { + return mInteractionFlags; } private void disconnectFromLauncherService() { @@ -260,5 +286,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public interface OverviewProxyListener { default void onConnectionChanged(boolean isConnected) {} default void onRecentsAnimationStarted() {} + default void onInteractionFlagsChanged(@InteractionType int flags) {} } } diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java index 9319bc60f9ef..adb4e33d1a19 100644 --- a/packages/SystemUI/src/com/android/systemui/Prefs.java +++ b/packages/SystemUI/src/com/android/systemui/Prefs.java @@ -49,7 +49,7 @@ public final class Prefs { Key.QS_NIGHTDISPLAY_ADDED, Key.SEEN_MULTI_USER, Key.NUM_APPS_LAUNCHED, - Key.HAS_SWIPED_UP_FOR_RECENTS, + Key.HAS_SEEN_RECENTS_ONBOARDING, }) public @interface Key { @Deprecated @@ -78,7 +78,7 @@ public final class Prefs { String QS_NIGHTDISPLAY_ADDED = "QsNightDisplayAdded"; String SEEN_MULTI_USER = "HasSeenMultiUser"; String NUM_APPS_LAUNCHED = "NumAppsLaunched"; - String HAS_SWIPED_UP_FOR_RECENTS = "HasSwipedUpForRecents"; + String HAS_SEEN_RECENTS_ONBOARDING = "HasSeenRecentsOnboarding"; } public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) { diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java index 0f3daf57e8c1..d1834e921d20 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java @@ -52,6 +52,7 @@ import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.DeviceProvisionedController; +import com.android.systemui.statusbar.policy.SmartReplyConstants; import java.util.function.Consumer; @@ -130,6 +131,8 @@ public class SystemUIFactory { providers.put(NotificationGutsManager.class, () -> new NotificationGutsManager(context)); providers.put(NotificationRemoteInputManager.class, () -> new NotificationRemoteInputManager(context)); + providers.put(SmartReplyConstants.class, + () -> new SmartReplyConstants(Dependency.get(Dependency.MAIN_HANDLER), context)); providers.put(NotificationListener.class, () -> new NotificationListener(context)); providers.put(NotificationLogger.class, NotificationLogger::new); providers.put(NotificationViewHierarchyManager.class, diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java index ddf0bd0cbab4..bb82a54c12f1 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java @@ -28,6 +28,8 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import com.android.internal.os.BinderInternal; +import com.android.systemui.plugins.PluginManager; +import com.android.systemui.plugins.PluginManagerImpl; public class SystemUIService extends Service { @@ -70,6 +72,10 @@ public class SystemUIService extends Service { pw.println("dumping service: " + ui.getClass().getName()); ui.dump(fd, pw, args); } + if (Build.IS_DEBUGGABLE) { + pw.println("dumping plugins:"); + ((PluginManagerImpl) Dependency.get(PluginManager.class)).dump(fd, pw, args); + } } else { String svc = args[0]; for (SystemUI ui: services) { diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java index 348855bb0440..afc9629ecede 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java +++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingAnimation.java @@ -19,7 +19,6 @@ package com.android.systemui.charging; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; -import android.content.res.Configuration; import android.graphics.PixelFormat; import android.os.Handler; import android.os.Looper; @@ -36,13 +35,18 @@ import android.view.WindowManager; */ public class WirelessChargingAnimation { - public static final long DURATION = 1400; + public static final long DURATION = 1133; private static final String TAG = "WirelessChargingView"; private static final boolean LOCAL_LOGV = false; private final WirelessChargingView mCurrentWirelessChargingView; private static WirelessChargingView mPreviousWirelessChargingView; + public interface Callback { + void onAnimationStarting(); + void onAnimationEnded(); + } + /** * Constructs an empty WirelessChargingAnimation object. If looper is null, * Looper.myLooper() is used. Must set @@ -51,9 +55,9 @@ public class WirelessChargingAnimation { * @hide */ public WirelessChargingAnimation(@NonNull Context context, @Nullable Looper looper, int - batteryLevel) { + batteryLevel, Callback callback) { mCurrentWirelessChargingView = new WirelessChargingView(context, looper, - batteryLevel); + batteryLevel, callback); } /** @@ -61,8 +65,8 @@ public class WirelessChargingAnimation { * @hide */ public static WirelessChargingAnimation makeWirelessChargingAnimation(@NonNull Context context, - @Nullable Looper looper, int batteryLevel) { - return new WirelessChargingAnimation(context, looper, batteryLevel); + @Nullable Looper looper, int batteryLevel, Callback callback) { + return new WirelessChargingAnimation(context, looper, batteryLevel, callback); } /** @@ -95,8 +99,11 @@ public class WirelessChargingAnimation { private View mView; private View mNextView; private WindowManager mWM; + private Callback mCallback; - public WirelessChargingView(Context context, @Nullable Looper looper, int batteryLevel) { + public WirelessChargingView(Context context, @Nullable Looper looper, int batteryLevel, + Callback callback) { + mCallback = callback; mNextView = new WirelessChargingLayout(context, batteryLevel); mGravity = Gravity.CENTER_HORIZONTAL | Gravity.CENTER; @@ -149,6 +156,8 @@ public class WirelessChargingAnimation { } public void hide(long duration) { + mHandler.removeMessages(HIDE); + if (LOCAL_LOGV) Log.v(TAG, "HIDE: " + this); mHandler.sendMessageDelayed(Message.obtain(mHandler, HIDE), duration); } @@ -169,18 +178,6 @@ public class WirelessChargingAnimation { context = mView.getContext(); } mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - // We can resolve the Gravity here by using the Locale for getting - // the layout direction - final Configuration config = mView.getContext().getResources().getConfiguration(); - final int gravity = Gravity.getAbsoluteGravity(mGravity, - config.getLayoutDirection()); - mParams.gravity = gravity; - if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { - mParams.horizontalWeight = 1.0f; - } - if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { - mParams.verticalWeight = 1.0f; - } mParams.packageName = packageName; mParams.hideTimeoutMilliseconds = DURATION; @@ -191,6 +188,9 @@ public class WirelessChargingAnimation { if (LOCAL_LOGV) Log.v(TAG, "ADD! " + mView + " in " + this); try { + if (mCallback != null) { + mCallback.onAnimationStarting(); + } mWM.addView(mView, mParams); } catch (WindowManager.BadTokenException e) { Slog.d(TAG, "Unable to add wireless charging view. " + e); @@ -203,6 +203,9 @@ public class WirelessChargingAnimation { if (mView != null) { if (mView.getParent() != null) { if (LOCAL_LOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); + if (mCallback != null) { + mCallback.onAnimationEnded(); + } mWM.removeViewImmediate(mView); } diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java index c78ea56524cb..8f87d647a0e3 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java +++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java @@ -16,13 +16,16 @@ package com.android.systemui.charging; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.util.AttributeSet; +import android.view.animation.PathInterpolator; import android.widget.FrameLayout; import android.widget.TextView; +import com.android.systemui.Interpolators; import com.android.systemui.R; import java.text.NumberFormat; @@ -52,10 +55,9 @@ public class WirelessChargingLayout extends FrameLayout { init(c, attrs, -1); } - private void init(Context c, AttributeSet attrs, int batteryLevel) { + private void init(Context context, AttributeSet attrs, int batteryLevel) { final int mBatteryLevel = batteryLevel; - - inflate(c, R.layout.wireless_charging_layout, this); + inflate(context, R.layout.wireless_charging_layout, this); // where the circle animation occurs: final WirelessChargingView mChargingView = findViewById(R.id.wireless_charging_view); @@ -68,14 +70,57 @@ public class WirelessChargingLayout extends FrameLayout { if (batteryLevel != UNKNOWN_BATTERY_LEVEL) { mPercentage.setText(NumberFormat.getPercentInstance().format(mBatteryLevel / 100f)); - - ValueAnimator animator = ObjectAnimator.ofFloat(mPercentage, "textSize", - getContext().getResources().getFloat(R.dimen.config_batteryLevelTextSizeStart), - getContext().getResources().getFloat(R.dimen.config_batteryLevelTextSizeEnd)); - - animator.setDuration((long) getContext().getResources().getInteger( - R.integer.config_batteryLevelTextAnimationDuration)); - animator.start(); + mPercentage.setAlpha(0); } + + final long chargingAnimationFadeStartOffset = (long) context.getResources().getInteger( + R.integer.wireless_charging_fade_offset); + final long chargingAnimationFadeDuration = (long) context.getResources().getInteger( + R.integer.wireless_charging_fade_duration); + final int batteryLevelTextSizeStart = context.getResources().getDimensionPixelSize( + R.dimen.wireless_charging_anim_battery_level_text_size_start); + final int batteryLevelTextSizeEnd = context.getResources().getDimensionPixelSize( + R.dimen.wireless_charging_anim_battery_level_text_size_end); + + // Animation Scale: battery percentage text scales from 0% to 100% + ValueAnimator textSizeAnimator = ObjectAnimator.ofFloat(mPercentage, "textSize", + batteryLevelTextSizeStart, batteryLevelTextSizeEnd); + textSizeAnimator.setInterpolator(new PathInterpolator(0, 0, 0, 1)); + textSizeAnimator.setDuration((long) context.getResources().getInteger( + R.integer.wireless_charging_battery_level_text_scale_animation_duration)); + + // Animation Opacity: battery percentage text transitions from 0 to 1 opacity + ValueAnimator textOpacityAnimator = ObjectAnimator.ofFloat(mPercentage, "alpha", 0, 1); + textOpacityAnimator.setInterpolator(Interpolators.LINEAR); + textOpacityAnimator.setDuration((long) context.getResources().getInteger( + R.integer.wireless_charging_battery_level_text_opacity_duration)); + textOpacityAnimator.setStartDelay((long) context.getResources().getInteger( + R.integer.wireless_charging_anim_opacity_offset)); + + // Animation Opacity: battery percentage text fades from 1 to 0 opacity + ValueAnimator textFadeAnimator = ObjectAnimator.ofFloat(mPercentage, "alpha", 1, 0); + textFadeAnimator.setDuration(chargingAnimationFadeDuration); + textFadeAnimator.setInterpolator(Interpolators.LINEAR); + textFadeAnimator.setStartDelay(chargingAnimationFadeStartOffset); + + // Animation Opacity: wireless charging circle animation fades from 1 to 0 opacity + ValueAnimator circleFadeAnimator = ObjectAnimator.ofFloat(mChargingView, "alpha", + 1, 0); + circleFadeAnimator.setDuration(chargingAnimationFadeDuration); + circleFadeAnimator.setInterpolator(Interpolators.LINEAR); + circleFadeAnimator.setStartDelay(chargingAnimationFadeStartOffset); + + // Animation Opacity: secondary text animation fades from 1 to 0 opacity + ValueAnimator secondaryTextFadeAnimator = ObjectAnimator.ofFloat(mSecondaryText, "alpha", + 1, 0); + circleFadeAnimator.setDuration(chargingAnimationFadeDuration); + secondaryTextFadeAnimator.setInterpolator(Interpolators.LINEAR); + secondaryTextFadeAnimator.setStartDelay(chargingAnimationFadeStartOffset); + + // play all animations together + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(textSizeAnimator, textOpacityAnimator, textFadeAnimator, + circleFadeAnimator, secondaryTextFadeAnimator); + animatorSet.start(); } -} +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingView.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingView.java index f5edf5216fa2..19c6dc1ceb1f 100644 --- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingView.java +++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingView.java @@ -21,10 +21,10 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; -import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import com.android.settingslib.Utils; +import com.android.systemui.Interpolators; import com.android.systemui.R; final class WirelessChargingView extends View { @@ -33,21 +33,21 @@ final class WirelessChargingView extends View { private float mPathGone; private float mInterpolatedPathGone; private long mAnimationStartTime; - private long mStartSpinCircleAnimationTime; - private long mAnimationOffset = 500; - private long mTotalAnimationDuration = WirelessChargingAnimation.DURATION - mAnimationOffset; - private long mExpandingCircle = (long) (mTotalAnimationDuration * .9); - private long mSpinCircleAnimationTime = mTotalAnimationDuration - mExpandingCircle; + private long mScaleDotsDuration; - private boolean mFinishedAnimatingSpinningCircles = false; + private boolean mFinishedAnimatingDots = false; + private int mNumDots; - private int mStartAngle = -90; - private int mNumSmallCircles = 20; - private int mSmallCircleRadius = 10; + private double mAngleOffset; + private double mCurrAngleOffset; - private int mMainCircleStartRadius = 100; - private int mMainCircleEndRadius = 230; - private int mMainCircleCurrentRadius = mMainCircleStartRadius; + private int mDotsRadiusStart; + private int mDotsRadiusEnd; + private int mCurrDotRadius; + + private int mMainCircleStartRadius; + private int mMainCircleEndRadius; + private int mMainCircleCurrentRadius; private int mCenterX; private int mCenterY; @@ -72,8 +72,25 @@ final class WirelessChargingView extends View { public void init(Context context, AttributeSet attr) { mContext = context; + + mDotsRadiusStart = context.getResources().getDimensionPixelSize( + R.dimen.wireless_charging_dots_radius_start); + mDotsRadiusEnd = context.getResources().getDimensionPixelSize( + R.dimen.wireless_charging_dots_radius_end); + + mMainCircleStartRadius = context.getResources().getDimensionPixelSize( + R.dimen.wireless_charging_circle_radius_start); + mMainCircleEndRadius = context.getResources().getDimensionPixelSize( + R.dimen.wireless_charging_circle_radius_end); + mMainCircleCurrentRadius = mMainCircleStartRadius; + + mAngleOffset = context.getResources().getInteger(R.integer.wireless_charging_angle_offset); + mScaleDotsDuration = (long) context.getResources().getInteger( + R.integer.wireless_charging_scale_dots_duration); + mNumDots = context.getResources().getInteger(R.integer.wireless_charging_num_dots); + setupPaint(); - mInterpolator = new DecelerateInterpolator(); + mInterpolator = Interpolators.LINEAR_OUT_SLOW_IN; } private void setupPaint() { @@ -92,64 +109,62 @@ final class WirelessChargingView extends View { updateDrawingParameters(); drawCircles(canvas); - if (!mFinishedAnimatingSpinningCircles) { + if (!mFinishedAnimatingDots) { invalidate(); } } /** * Draws a larger circle of radius {@link WirelessChargingView#mMainCircleEndRadius} composed of - * {@link WirelessChargingView#mNumSmallCircles} smaller circles + * {@link WirelessChargingView#mNumDots} smaller circles * @param canvas */ private void drawCircles(Canvas canvas) { mCenterX = canvas.getWidth() / 2; mCenterY = canvas.getHeight() / 2; - // angleOffset makes small circles look like they're moving around the main circle - float angleOffset = mPathGone * 10; - - // draws mNumSmallCircles to compose a larger, main circle - for (int circle = 0; circle < mNumSmallCircles; circle++) { - double angle = ((mStartAngle + angleOffset) * Math.PI / 180) + (circle * ((2 * Math.PI) - / mNumSmallCircles)); + // draws mNumDots to compose a larger, main circle + for (int circle = 0; circle < mNumDots; circle++) { + double angle = ((mCurrAngleOffset) * Math.PI / 180) + (circle * ((2 * Math.PI) + / mNumDots)); int x = (int) (mCenterX + Math.cos(angle) * (mMainCircleCurrentRadius + - mSmallCircleRadius)); + mCurrDotRadius)); int y = (int) (mCenterY + Math.sin(angle) * (mMainCircleCurrentRadius + - mSmallCircleRadius)); - - canvas.drawCircle(x, y, mSmallCircleRadius, mPaint); - } + mCurrDotRadius)); - if (mMainCircleCurrentRadius >= mMainCircleEndRadius && !isSpinCircleAnimationStarted()) { - mStartSpinCircleAnimationTime = System.currentTimeMillis(); + canvas.drawCircle(x, y, mCurrDotRadius, mPaint); } - if (isSpinAnimationFinished()) { - mFinishedAnimatingSpinningCircles = true; + if (mMainCircleCurrentRadius >= mMainCircleEndRadius) { + mFinishedAnimatingDots = true; } } - private boolean isSpinCircleAnimationStarted() { - return mStartSpinCircleAnimationTime != 0; - } - - private boolean isSpinAnimationFinished() { - return isSpinCircleAnimationStarted() && System.currentTimeMillis() - - mStartSpinCircleAnimationTime > mSpinCircleAnimationTime; - } - private void updateDrawingParameters() { - mPathGone = getPathGone(System.currentTimeMillis()); + long now = System.currentTimeMillis(); + long timeSinceStart = now - mAnimationStartTime; + mPathGone = getPathGone(now); mInterpolatedPathGone = mInterpolator.getInterpolation(mPathGone); + // Position Dots: update main circle radius (that the dots compose) if (mPathGone < 1.0f) { mMainCircleCurrentRadius = mMainCircleStartRadius + (int) (mInterpolatedPathGone * (mMainCircleEndRadius - mMainCircleStartRadius)); } else { mMainCircleCurrentRadius = mMainCircleEndRadius; } + + // Scale Dots: update dot radius + if (timeSinceStart < mScaleDotsDuration) { + mCurrDotRadius = mDotsRadiusStart + (int) (mInterpolator.getInterpolation((float) + timeSinceStart / mScaleDotsDuration) * (mDotsRadiusEnd - mDotsRadiusStart)); + } else { + mCurrDotRadius = mDotsRadiusEnd; + } + + // Rotation Dot Group: dots rotate from 0 to 20 degrees + mCurrAngleOffset = mAngleOffset * mPathGone; } /** @@ -158,6 +173,6 @@ final class WirelessChargingView extends View { * For values > 1.0 the larger circle has been drawn and further animation can occur */ private float getPathGone(long now) { - return (float) (now - mAnimationStartTime) / (mExpandingCircle); + return (float) (now - mAnimationStartTime) / (WirelessChargingAnimation.DURATION); } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java b/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java index bfb3a6ea55f7..092f3d241d6c 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeFactory.java @@ -64,7 +64,7 @@ public class DozeFactory { createDozeTriggers(context, sensorManager, host, alarmManager, config, params, handler, wakeLock, machine), createDozeUi(context, host, wakeLock, machine, handler, alarmManager, params), - new DozeScreenState(wrappedService, handler), + new DozeScreenState(wrappedService, handler, params), createDozeScreenBrightness(context, wrappedService, sensorManager, host, handler), new DozeWallpaperState(context) }); diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java index 8ec6afc326e1..6ff8e3db25e5 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java @@ -24,6 +24,7 @@ import android.view.Display; import com.android.internal.hardware.AmbientDisplayConfiguration; import com.android.internal.util.Preconditions; +import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.util.Assert; import com.android.systemui.util.wakelock.WakeLock; @@ -87,12 +88,11 @@ public class DozeMachine { } } - int screenState() { + int screenState(DozeParameters parameters) { switch (this) { case UNINITIALIZED: case INITIALIZED: case DOZE: - case DOZE_REQUEST_PULSE: case DOZE_AOD_PAUSED: return Display.STATE_OFF; case DOZE_PULSING: @@ -100,6 +100,9 @@ public class DozeMachine { case DOZE_AOD: case DOZE_AOD_PAUSING: return Display.STATE_DOZE_SUSPEND; + case DOZE_REQUEST_PULSE: + return parameters.getDisplayNeedsBlanking() ? Display.STATE_OFF + : Display.STATE_ON; default: return Display.STATE_UNKNOWN; } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java index bef9cb38180f..3053de366be5 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java @@ -19,6 +19,8 @@ package com.android.systemui.doze; import android.os.Handler; import android.view.Display; +import com.android.systemui.statusbar.phone.DozeParameters; + /** * Controls the screen when dozing. */ @@ -26,17 +28,20 @@ public class DozeScreenState implements DozeMachine.Part { private final DozeMachine.Service mDozeService; private final Handler mHandler; private final Runnable mApplyPendingScreenState = this::applyPendingScreenState; + private final DozeParameters mParameters; private int mPendingScreenState = Display.STATE_UNKNOWN; - public DozeScreenState(DozeMachine.Service service, Handler handler) { + public DozeScreenState(DozeMachine.Service service, Handler handler, + DozeParameters parameters) { mDozeService = service; mHandler = handler; + mParameters = parameters; } @Override public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) { - int screenState = newState.screenState(); + int screenState = newState.screenState(mParameters); if (newState == DozeMachine.State.FINISH) { // Make sure not to apply the screen state after DozeService was destroyed. diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java index 34d392861d7c..aa264190f800 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java @@ -59,6 +59,12 @@ public class DozeService extends DreamService } @Override + public void onDestroy() { + Dependency.get(PluginManager.class).removePluginListener(this); + super.onDestroy(); + } + + @Override public void onPluginConnected(DozeServicePlugin plugin, Context pluginContext) { mDozePlugin = plugin; mDozePlugin.setDozeRequester(this); diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java index c28b7eed1614..259bff28d458 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java @@ -688,7 +688,7 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener, } private Action getLockdownAction() { - return new SinglePressAction(R.drawable.ic_lock_lock, + return new SinglePressAction(com.android.systemui.R.drawable.ic_lock_lockdown, R.string.global_action_lockdown) { @Override @@ -1369,6 +1369,7 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener, mListView = findViewById(android.R.id.list); mHardwareLayout = HardwareUiLayout.get(mListView); mHardwareLayout.setOutsideTouchListener(view -> dismiss()); + setTitle(R.string.global_actions); } private void updateList() { @@ -1464,20 +1465,6 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener, } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { - for (int i = 0; i < mAdapter.getCount(); ++i) { - CharSequence label = - mAdapter.getItem(i).getLabelForAccessibility(getContext()); - if (label != null) { - event.getText().add(label); - } - } - } - return super.dispatchPopulateAccessibilityEvent(event); - } - - @Override public void onColorsChanged(ColorExtractor extractor, int which) { if (mKeyguardShowing) { if ((WallpaperManager.FLAG_LOCK & which) != 0) { diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java index 82c0128c10c0..d5541e9be17e 100644 --- a/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java +++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginInstanceManager.java @@ -168,6 +168,12 @@ public class PluginInstanceManager<T extends Plugin> { return false; } + @Override + public String toString() { + return String.format("%s@%s (action=%s)", + getClass().getSimpleName(), hashCode(), mAction); + } + private class MainHandler extends Handler { private static final int PLUGIN_CONNECTED = 1; private static final int PLUGIN_DISCONNECTED = 2; diff --git a/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java b/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java index 03747d50a6fa..2a17e35f00dd 100644 --- a/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/plugins/PluginManagerImpl.java @@ -50,6 +50,8 @@ import com.android.systemui.plugins.annotations.ProvidesInterface; import dalvik.system.PathClassLoader; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.lang.Thread.UncaughtExceptionHandler; import java.util.Map; @@ -303,6 +305,14 @@ public class PluginManagerImpl extends BroadcastReceiver implements PluginManage } } + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println(String.format(" plugin map (%d):", mPluginMap.size())); + for (PluginListener listener: mPluginMap.keySet()) { + pw.println(String.format(" %s -> %s", + listener, mPluginMap.get(listener))); + } + } + @VisibleForTesting public static class PluginInstanceManagerFactory { public <T extends Plugin> PluginInstanceManager createPluginInstanceManager(Context context, diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index aa56694775fc..3a2b12f4da23 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -26,10 +26,6 @@ import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnDismissListener; import android.content.Intent; import android.content.IntentFilter; -import android.icu.text.MeasureFormat; -import android.icu.text.MeasureFormat.FormatWidth; -import android.icu.util.Measure; -import android.icu.util.MeasureUnit; import android.media.AudioAttributes; import android.os.AsyncTask; import android.os.Handler; @@ -37,11 +33,11 @@ import android.os.Looper; import android.os.PowerManager; import android.os.UserHandle; import android.support.annotation.VisibleForTesting; -import android.text.format.DateUtils; import android.util.Slog; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.settingslib.Utils; +import com.android.settingslib.utils.PowerUtil; import com.android.systemui.R; import com.android.systemui.SystemUI; import com.android.systemui.statusbar.phone.SystemUIDialog; @@ -49,8 +45,6 @@ import com.android.systemui.util.NotificationChannels; import java.io.PrintWriter; import java.text.NumberFormat; -import java.util.Locale; -import java.util.concurrent.TimeUnit; public class PowerNotificationWarnings implements PowerUI.WarningsUI { private static final String TAG = PowerUI.TAG + ".Notification"; @@ -200,12 +194,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { // override notification copy if hybrid notification enabled if (mEstimate != null) { title = mContext.getString(R.string.battery_low_title_hybrid); - contentText = mContext.getString( - mEstimate.isBasedOnUsage - ? R.string.battery_low_percent_format_hybrid - : R.string.battery_low_percent_format_hybrid_short, - percentage, - getTimeRemainingFormatted()); + contentText = getHybridContentString(percentage); } final Notification.Builder nb = @@ -239,21 +228,12 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, n, UserHandle.ALL); } - @VisibleForTesting - String getTimeRemainingFormatted() { - final Locale currentLocale = mContext.getResources().getConfiguration().getLocales().get(0); - MeasureFormat frmt = MeasureFormat.getInstance(currentLocale, FormatWidth.NARROW); - - final long remainder = mEstimate.estimateMillis % DateUtils.HOUR_IN_MILLIS; - final long hours = TimeUnit.MILLISECONDS.toHours( - mEstimate.estimateMillis - remainder); - // round down to the nearest 15 min for now to not appear overly precise - final long minutes = TimeUnit.MILLISECONDS.toMinutes( - remainder - (remainder % TimeUnit.MINUTES.toMillis(15))); - final Measure hoursMeasure = new Measure(hours, MeasureUnit.HOUR); - final Measure minutesMeasure = new Measure(minutes, MeasureUnit.MINUTE); - - return frmt.formatMeasures(hoursMeasure, minutesMeasure); + private String getHybridContentString(String percentage) { + return PowerUtil.getBatteryRemainingStringFormatted( + mContext, + mEstimate.estimateMillis, + percentage, + mEstimate.isBasedOnUsage); } private PendingIntent pendingBroadcast(String action) { diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index ea2a432ea6ca..ac86c8ae097d 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -53,7 +53,6 @@ import com.android.systemui.statusbar.phone.StatusBar; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Arrays; -import java.util.concurrent.TimeUnit; public class PowerUI extends SystemUI { static final String TAG = "PowerUI"; @@ -72,10 +71,11 @@ public class PowerUI extends SystemUI { private final Configuration mLastConfiguration = new Configuration(); private int mBatteryLevel = 100; private long mTimeRemaining = Long.MAX_VALUE; - private int mBatteryStatus = BatteryManager.BATTERY_STATUS_UNKNOWN; private int mPlugType = 0; private int mInvalidCharger = 0; private EnhancedEstimates mEnhancedEstimates; + private boolean mLowWarningShownThisChargeCycle; + private boolean mSevereWarningShownThisChargeCycle; private int mLowBatteryAlertCloseLevel; private final int[] mLowBatteryReminderLevels = new int[2]; @@ -88,6 +88,8 @@ public class PowerUI extends SystemUI { private long mNextLogTime; private IThermalService mThermalService; + @VisibleForTesting int mBatteryStatus = BatteryManager.BATTERY_STATUS_UNKNOWN; + // by using the same instance (method references are not guaranteed to be the same object // We create a method reference here so that we are guaranteed that we can remove a callback // each time they are created). @@ -218,6 +220,12 @@ public class PowerUI extends SystemUI { final boolean plugged = mPlugType != 0; final boolean oldPlugged = oldPlugType != 0; + // if we are now unplugged but we were previously plugged in we should allow the + // time based trigger again. + if (!plugged && plugged != oldPlugged) { + mLowWarningShownThisChargeCycle = false; + mSevereWarningShownThisChargeCycle = false; + } int oldBucket = findBatteryLevelBucket(oldBatteryLevel); int bucket = findBatteryLevelBucket(mBatteryLevel); @@ -268,7 +276,6 @@ public class PowerUI extends SystemUI { boolean isPowerSaver = mPowerManager.isPowerSaveMode(); // only play SFX when the dialog comes up or the bucket changes final boolean playSound = bucket != oldBucket || oldPlugged; - long oldTimeRemaining = mTimeRemaining; if (mEnhancedEstimates.isHybridNotificationEnabled()) { final Estimate estimate = mEnhancedEstimates.getEstimate(); // Turbo is not always booted once SysUI is running so we have ot make sure we actually @@ -281,10 +288,18 @@ public class PowerUI extends SystemUI { } } - if (shouldShowLowBatteryWarning(plugged, oldPlugged, oldBucket, bucket, oldTimeRemaining, - mTimeRemaining, - isPowerSaver, mBatteryStatus)) { + if (shouldShowLowBatteryWarning(plugged, oldPlugged, oldBucket, bucket, + mTimeRemaining, isPowerSaver, mBatteryStatus)) { mWarnings.showLowBatteryWarning(playSound); + + // mark if we've already shown a warning this cycle. This will prevent the time based + // trigger from spamming users since the time remaining can vary based on current + // device usage. + if (mTimeRemaining < mEnhancedEstimates.getSevereWarningThreshold()) { + mSevereWarningShownThisChargeCycle = true; + } else { + mLowWarningShownThisChargeCycle = true; + } } else if (shouldDismissLowBatteryWarning(plugged, oldBucket, bucket, mTimeRemaining, isPowerSaver)) { mWarnings.dismissLowBatteryWarning(); @@ -295,22 +310,14 @@ public class PowerUI extends SystemUI { @VisibleForTesting boolean shouldShowLowBatteryWarning(boolean plugged, boolean oldPlugged, int oldBucket, - int bucket, long oldTimeRemaining, long timeRemaining, - boolean isPowerSaver, int mBatteryStatus) { + int bucket, long timeRemaining, boolean isPowerSaver, int mBatteryStatus) { return !plugged && !isPowerSaver && (((bucket < oldBucket || oldPlugged) && bucket < 0) - || (mEnhancedEstimates.isHybridNotificationEnabled() - && timeRemaining < mEnhancedEstimates.getLowWarningThreshold() - && isHourLess(oldTimeRemaining, timeRemaining))) + || isTimeBasedTrigger(timeRemaining)) && mBatteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN; } - private boolean isHourLess(long oldTimeRemaining, long timeRemaining) { - final long dif = oldTimeRemaining - timeRemaining; - return dif >= TimeUnit.HOURS.toMillis(1); - } - @VisibleForTesting boolean shouldDismissLowBatteryWarning(boolean plugged, int oldBucket, int bucket, long timeRemaining, boolean isPowerSaver) { @@ -323,6 +330,23 @@ public class PowerUI extends SystemUI { || hybridWouldDismiss)); } + private boolean isTimeBasedTrigger(long timeRemaining) { + if (!mEnhancedEstimates.isHybridNotificationEnabled()) { + return false; + } + + // Only show the time based warning once per charge cycle + final boolean canShowWarning = timeRemaining < mEnhancedEstimates.getLowWarningThreshold() + && !mLowWarningShownThisChargeCycle; + + // Only show the severe time based warning once per charge cycle + final boolean canShowSevereWarning = + timeRemaining < mEnhancedEstimates.getSevereWarningThreshold() + && !mSevereWarningShownThisChargeCycle; + + return canShowWarning || canShowSevereWarning; + } + private void initTemperatureWarning() { ContentResolver resolver = mContext.getContentResolver(); Resources resources = mContext.getResources(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/CellTileView.java b/packages/SystemUI/src/com/android/systemui/qs/CellTileView.java index 5f2609380085..e7eefe8d5e56 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/CellTileView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/CellTileView.java @@ -19,12 +19,12 @@ import android.graphics.drawable.Drawable; import android.service.quicksettings.Tile; import android.widget.ImageView; +import com.android.settingslib.graph.SignalDrawable; import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.plugins.qs.QSTile.Icon; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.tileimpl.QSTileImpl; -import com.android.systemui.statusbar.phone.SignalDrawable; import java.util.Objects; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java index fe3ffb926305..e24135775e79 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java @@ -174,7 +174,9 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, .addFloat(mDivider, "alpha", 0, 1) .addFloat(mCarrierText, "alpha", 0, 1) .addFloat(mActionsContainer, "alpha", 0, 1) - .addFloat(mDragHandle, "translationY", 0, -mDragHandleExpandOffset) + .addFloat(mDragHandle, "translationY", mDragHandleExpandOffset, 0) + .addFloat(mDragHandle, "alpha", 1, 0) + .setStartDelay(0.15f) .build(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSScrollLayout.java b/packages/SystemUI/src/com/android/systemui/qs/QSScrollLayout.java index 9cda75c858e8..7b1509dcd173 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSScrollLayout.java @@ -14,8 +14,11 @@ package com.android.systemui.qs; +import android.animation.ObjectAnimator; import android.content.Context; +import android.graphics.Canvas; import android.support.v4.widget.NestedScrollView; +import android.util.Property; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; @@ -23,6 +26,8 @@ import android.view.ViewParent; import android.widget.LinearLayout; import com.android.systemui.R; +import com.android.systemui.qs.touch.OverScroll; +import com.android.systemui.qs.touch.SwipeDetector; /** * Quick setting scroll view containing the brightness slider and the QS tiles. @@ -35,6 +40,9 @@ public class QSScrollLayout extends NestedScrollView { private final int mTouchSlop; private final int mFooterHeight; private int mLastMotionY; + private final SwipeDetector mSwipeDetector; + private final OverScrollHelper mOverScrollHelper; + private float mContentTranslationY; public QSScrollLayout(Context context, View... children) { super(context); @@ -49,6 +57,35 @@ public class QSScrollLayout extends NestedScrollView { linearLayout.addView(view); } addView(linearLayout); + setOverScrollMode(OVER_SCROLL_NEVER); + mOverScrollHelper = new OverScrollHelper(); + mSwipeDetector = new SwipeDetector(context, mOverScrollHelper, SwipeDetector.VERTICAL); + mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, true); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (!canScrollVertically(1) && !canScrollVertically(-1)) { + return false; + } + mSwipeDetector.onTouchEvent(ev); + return super.onInterceptTouchEvent(ev) || mOverScrollHelper.isInOverScroll(); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!canScrollVertically(1) && !canScrollVertically(-1)) { + return false; + } + mSwipeDetector.onTouchEvent(ev); + return super.onTouchEvent(ev); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + canvas.translate(0, mContentTranslationY); + super.dispatchDraw(canvas); + canvas.translate(0, -mContentTranslationY); } public boolean shouldIntercept(MotionEvent ev) { @@ -81,4 +118,81 @@ public class QSScrollLayout extends NestedScrollView { parent.requestDisallowInterceptTouchEvent(disallowIntercept); } } + + private void setContentTranslationY(float contentTranslationY) { + mContentTranslationY = contentTranslationY; + invalidate(); + } + + private static final Property<QSScrollLayout, Float> CONTENT_TRANS_Y = + new Property<QSScrollLayout, Float>(Float.class, "qsScrollLayoutContentTransY") { + @Override + public Float get(QSScrollLayout qsScrollLayout) { + return qsScrollLayout.mContentTranslationY; + } + + @Override + public void set(QSScrollLayout qsScrollLayout, Float y) { + qsScrollLayout.setContentTranslationY(y); + } + }; + + private class OverScrollHelper implements SwipeDetector.Listener { + private boolean mIsInOverScroll; + + // We use this value to calculate the actual amount the user has overscrolled. + private float mFirstDisplacement = 0; + + @Override + public void onDragStart(boolean start) {} + + @Override + public boolean onDrag(float displacement, float velocity) { + // Only overscroll if the user is scrolling down when they're already at the bottom + // or scrolling up when they're already at the top. + boolean wasInOverScroll = mIsInOverScroll; + mIsInOverScroll = (!canScrollVertically(1) && displacement < 0) || + (!canScrollVertically(-1) && displacement > 0); + + if (wasInOverScroll && !mIsInOverScroll) { + // Exit overscroll. This can happen when the user is in overscroll and then + // scrolls the opposite way. Note that this causes the reset translation animation + // to run while the user is dragging, which feels a bit unnatural. + reset(); + } else if (mIsInOverScroll) { + if (Float.compare(mFirstDisplacement, 0) == 0) { + // Because users can scroll before entering overscroll, we need to + // subtract the amount where the user was not in overscroll. + mFirstDisplacement = displacement; + } + float overscrollY = displacement - mFirstDisplacement; + setContentTranslationY(getDampedOverScroll(overscrollY)); + } + + return mIsInOverScroll; + } + + @Override + public void onDragEnd(float velocity, boolean fling) { + reset(); + } + + private void reset() { + if (Float.compare(mContentTranslationY, 0) != 0) { + ObjectAnimator.ofFloat(QSScrollLayout.this, CONTENT_TRANS_Y, 0) + .setDuration(100) + .start(); + } + mIsInOverScroll = false; + mFirstDisplacement = 0; + } + + public boolean isInOverScroll() { + return mIsInOverScroll; + } + + private float getDampedOverScroll(float y) { + return OverScroll.dampedScroll(y, getHeight()); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java index 3b9e7bcfb9b4..65135ab142d7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java @@ -104,6 +104,11 @@ public class TileLayout extends ViewGroup implements QSTileLayout { setMeasuredDimension(width, height); } + @Override + public boolean hasOverlappingRendering() { + return false; + } + private static int exactly(int size) { return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); } 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 5a3081cd6664..3847040271e2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java @@ -171,8 +171,6 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene queryTiles(); mNotifQsContainer.setCustomizerAnimating(true); mNotifQsContainer.setCustomizerShowing(true); - announceForAccessibility(mContext.getString( - R.string.accessibility_desc_quick_settings_edit)); Dependency.get(KeyguardMonitor.class).addCallback(mKeyguardCallback); updateNavColors(); } @@ -213,8 +211,6 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene mClipper.animateCircularClip(mX, mY, false, mCollapseAnimationListener); mNotifQsContainer.setCustomizerAnimating(true); mNotifQsContainer.setCustomizerShowing(false); - announceForAccessibility(mContext.getString( - R.string.accessibility_desc_quick_settings)); Dependency.get(KeyguardMonitor.class).removeCallback(mKeyguardCallback); updateNavColors(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java index 2607ebbb72ea..2a9a3818a746 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java @@ -78,6 +78,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { @Override public void handleSetListening(boolean listening) { + if (mController == null) return; if (listening) { mController.addCallback(mCallback); } else { @@ -131,21 +132,10 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { if (enabled) { if (connected) { - state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_connected); + state.icon = new BluetoothConnectedTileIcon(); state.contentDescription = mContext.getString( R.string.accessibility_bluetooth_name, state.label); - final CachedBluetoothDevice lastDevice = mController.getLastDevice(); - if (lastDevice != null) { - final int batteryLevel = lastDevice.getBatteryLevel(); - if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { - state.icon = new BluetoothBatteryTileIcon( - batteryLevel, - mContext.getResources().getFraction( - R.fraction.bt_battery_scale_fraction, 1, 1)); - } - } - state.label = mController.getLastDeviceName(); } else if (state.isTransient) { state.icon = ResourceIcon.get(R.drawable.ic_bluetooth_transient_animation); @@ -281,6 +271,25 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { } } + + /** + * Bluetooth icon wrapper (when connected with no battery indicator) for Quick Settings. This is + * used instead of {@link com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon} in order to + * use a context that reflects dark/light theme attributes. + */ + private class BluetoothConnectedTileIcon extends Icon { + + BluetoothConnectedTileIcon() { + // Do nothing. Default constructor to limit visibility. + } + + @Override + public Drawable getDrawable(Context context) { + // This method returns Pair<Drawable, String> - the first value is the drawable. + return context.getDrawable(R.drawable.ic_qs_bluetooth_connected); + } + } + protected class BluetoothDetailAdapter implements DetailAdapter, QSDetailItems.Callback { // We probably won't ever have space in the UI for more than 20 devices, so don't // get info for them. diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java index 6205e9afcb03..2d31669db6c2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java @@ -240,6 +240,7 @@ public class DndTile extends QSTileImpl<BooleanState> { public void handleSetListening(boolean listening) { if (mListening == listening) return; mListening = listening; + if (mController == null) return; if (mListening) { mController.addCallback(mZenCallback); Prefs.registerListener(mContext, mPrefListener); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java index b3ff4e5b890c..12daff1f12f9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java @@ -98,6 +98,8 @@ public class NfcTile extends QSTileImpl<BooleanState> { protected void handleUpdateState(BooleanState state, Object arg) { final Drawable mEnable = mContext.getDrawable(R.drawable.ic_qs_nfc_enabled); final Drawable mDisable = mContext.getDrawable(R.drawable.ic_qs_nfc_disabled); + + if (getAdapter() == null) return; state.value = getAdapter().isEnabled(); state.label = mContext.getString(R.string.quick_settings_nfc_label); state.icon = new DrawableIcon(state.value ? mEnable : mDisable); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java index ea6e174d786e..3597929229e6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java @@ -21,6 +21,7 @@ import android.app.ActivityManager; import android.content.Intent; import android.provider.Settings; import android.service.quicksettings.Tile; +import android.support.annotation.StringRes; import android.widget.Switch; import com.android.internal.app.ColorDisplayController; @@ -30,6 +31,7 @@ import com.android.systemui.qs.QSHost; import com.android.systemui.plugins.qs.QSTile.BooleanState; import com.android.systemui.qs.tileimpl.QSTileImpl; +import java.time.LocalTime; import java.time.format.DateTimeFormatter; public class NightDisplayTile extends QSTileImpl<BooleanState> @@ -39,7 +41,9 @@ public class NightDisplayTile extends QSTileImpl<BooleanState> * Pattern for {@link java.time.format.DateTimeFormatter} used to approximate the time to the * nearest hour and add on the AM/PM indicator. */ - private static final String APPROXIMATE_HOUR_DATE_TIME_PATTERN = "h a"; + private static final String HOUR_MINUTE_DATE_TIME_PATTERN = "h a"; + private static final String APPROXIMATE_HOUR_DATE_TIME_PATTERN = "h:m a"; + private ColorDisplayController mController; private boolean mIsListening; @@ -110,17 +114,26 @@ public class NightDisplayTile extends QSTileImpl<BooleanState> case ColorDisplayController.AUTO_MODE_CUSTOM: // User-specified time, approximated to the nearest hour. - return isNightLightActivated - ? mContext.getString( - R.string.quick_settings_night_secondary_label_until, - mController.getCustomEndTime().format( - DateTimeFormatter.ofPattern( - APPROXIMATE_HOUR_DATE_TIME_PATTERN))) - : mContext.getString( - R.string.quick_settings_night_secondary_label_on_at, - mController.getCustomStartTime().format( - DateTimeFormatter.ofPattern( - APPROXIMATE_HOUR_DATE_TIME_PATTERN))); + final @StringRes int toggleTimeStringRes; + final LocalTime toggleTime; + final DateTimeFormatter toggleTimeFormat; + + if (isNightLightActivated) { + toggleTime = mController.getCustomEndTime(); + toggleTimeStringRes = R.string.quick_settings_night_secondary_label_until; + } else { + toggleTime = mController.getCustomStartTime(); + toggleTimeStringRes = R.string.quick_settings_night_secondary_label_on_at; + } + + // Choose between just showing the hour or also showing the minutes (based on the + // user-selected toggle time). This helps reduce how much space the label takes. + toggleTimeFormat = DateTimeFormatter.ofPattern( + toggleTime.getMinute() == 0 + ? HOUR_MINUTE_DATE_TIME_PATTERN + : APPROXIMATE_HOUR_DATE_TIME_PATTERN); + + return mContext.getString(toggleTimeStringRes, toggleTime.format(toggleTimeFormat)); default: // No secondary label when auto mode is disabled. diff --git a/packages/SystemUI/src/com/android/systemui/qs/touch/OverScroll.java b/packages/SystemUI/src/com/android/systemui/qs/touch/OverScroll.java new file mode 100644 index 000000000000..046488679725 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/touch/OverScroll.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 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.systemui.qs.touch; + +/** + * Utility methods for overscroll damping and related effect. + * + * Copied from packages/apps/Launcher3/src/com/android/launcher3/touch/OverScroll.java + */ +public class OverScroll { + + private static final float OVERSCROLL_DAMP_FACTOR = 0.07f; + + /** + * This curve determines how the effect of scrolling over the limits of the page diminishes + * as the user pulls further and further from the bounds + * + * @param f The percentage of how much the user has overscrolled. + * @return A transformed percentage based on the influence curve. + */ + private static float overScrollInfluenceCurve(float f) { + f -= 1.0f; + return f * f * f + 1.0f; + } + + /** + * @param amount The original amount overscrolled. + * @param max The maximum amount that the View can overscroll. + * @return The dampened overscroll amount. + */ + public static int dampedScroll(float amount, int max) { + if (Float.compare(amount, 0) == 0) return 0; + + float f = amount / max; + f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); + + // Clamp this factor, f, to -1 < f < 1 + if (Math.abs(f) >= 1) { + f /= Math.abs(f); + } + + return Math.round(OVERSCROLL_DAMP_FACTOR * f * max); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/touch/SwipeDetector.java b/packages/SystemUI/src/com/android/systemui/qs/touch/SwipeDetector.java new file mode 100644 index 000000000000..252205201e5d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/touch/SwipeDetector.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 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.systemui.qs.touch; + +import static android.view.MotionEvent.INVALID_POINTER_ID; + +import android.content.Context; +import android.graphics.PointF; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.util.Log; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +/** + * One dimensional scroll/drag/swipe gesture detector. + * + * Definition of swipe is different from android system in that this detector handles + * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before + * swipe action happens + * + * Copied from packages/apps/Launcher3/src/com/android/launcher3/touch/SwipeDetector.java + */ +public class SwipeDetector { + + private static final boolean DBG = false; + private static final String TAG = "SwipeDetector"; + + private int mScrollConditions; + public static final int DIRECTION_POSITIVE = 1 << 0; + public static final int DIRECTION_NEGATIVE = 1 << 1; + public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE; + + private static final float ANIMATION_DURATION = 1200; + + protected int mActivePointerId = INVALID_POINTER_ID; + + /** + * The minimum release velocity in pixels per millisecond that triggers fling.. + */ + public static final float RELEASE_VELOCITY_PX_MS = 1.0f; + + /** + * The time constant used to calculate dampening in the low-pass filter of scroll velocity. + * Cutoff frequency is set at 10 Hz. + */ + public static final float SCROLL_VELOCITY_DAMPENING_RC = 1000f / (2f * (float) Math.PI * 10); + + /* Scroll state, this is set to true during dragging and animation. */ + private ScrollState mState = ScrollState.IDLE; + + enum ScrollState { + IDLE, + DRAGGING, // onDragStart, onDrag + SETTLING // onDragEnd + } + + public static abstract class Direction { + + abstract float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint); + + /** + * Distance in pixels a touch can wander before we think the user is scrolling. + */ + abstract float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos); + } + + public static final Direction VERTICAL = new Direction() { + + @Override + float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint) { + return ev.getY(pointerIndex) - refPoint.y; + } + + @Override + float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) { + return Math.abs(ev.getX(pointerIndex) - downPos.x); + } + }; + + public static final Direction HORIZONTAL = new Direction() { + + @Override + float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint) { + return ev.getX(pointerIndex) - refPoint.x; + } + + @Override + float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) { + return Math.abs(ev.getY(pointerIndex) - downPos.y); + } + }; + + //------------------- ScrollState transition diagram ----------------------------------- + // + // IDLE -> (mDisplacement > mTouchSlop) -> DRAGGING + // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING + // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING + // SETTLING -> (View settled) -> IDLE + + private void setState(ScrollState newState) { + if (DBG) { + Log.d(TAG, "setState:" + mState + "->" + newState); + } + // onDragStart and onDragEnd is reported ONLY on state transition + if (newState == ScrollState.DRAGGING) { + initializeDragging(); + if (mState == ScrollState.IDLE) { + reportDragStart(false /* recatch */); + } else if (mState == ScrollState.SETTLING) { + reportDragStart(true /* recatch */); + } + } + if (newState == ScrollState.SETTLING) { + reportDragEnd(); + } + + mState = newState; + } + + public boolean isDraggingOrSettling() { + return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING; + } + + /** + * There's no touch and there's no animation. + */ + public boolean isIdleState() { + return mState == ScrollState.IDLE; + } + + public boolean isSettlingState() { + return mState == ScrollState.SETTLING; + } + + public boolean isDraggingState() { + return mState == ScrollState.DRAGGING; + } + + private final PointF mDownPos = new PointF(); + private final PointF mLastPos = new PointF(); + private final Direction mDir; + + private final float mTouchSlop; + + /* Client of this gesture detector can register a callback. */ + private final Listener mListener; + + private long mCurrentMillis; + + private float mVelocity; + private float mLastDisplacement; + private float mDisplacement; + + private float mSubtractDisplacement; + private boolean mIgnoreSlopWhenSettling; + + public interface Listener { + void onDragStart(boolean start); + + boolean onDrag(float displacement, float velocity); + + void onDragEnd(float velocity, boolean fling); + } + + public SwipeDetector(@NonNull Context context, @NonNull Listener l, @NonNull Direction dir) { + this(ViewConfiguration.get(context).getScaledTouchSlop(), l, dir); + } + + @VisibleForTesting + protected SwipeDetector(float touchSlope, @NonNull Listener l, @NonNull Direction dir) { + mTouchSlop = touchSlope; + mListener = l; + mDir = dir; + } + + public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) { + mScrollConditions = scrollDirectionFlags; + mIgnoreSlopWhenSettling = ignoreSlop; + } + + private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) { + // reject cases where the angle or slop condition is not met. + if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop) + > Math.abs(mDisplacement)) { + return false; + } + + // Check if the client is interested in scroll in current direction. + if (((mScrollConditions & DIRECTION_NEGATIVE) > 0 && mDisplacement > 0) || + ((mScrollConditions & DIRECTION_POSITIVE) > 0 && mDisplacement < 0)) { + return true; + } + return false; + } + + public boolean onTouchEvent(MotionEvent ev) { + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = ev.getPointerId(0); + mDownPos.set(ev.getX(), ev.getY()); + mLastPos.set(mDownPos); + mLastDisplacement = 0; + mDisplacement = 0; + mVelocity = 0; + + if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { + setState(ScrollState.DRAGGING); + } + break; + //case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_POINTER_UP: + int ptrIdx = ev.getActionIndex(); + int ptrId = ev.getPointerId(ptrIdx); + if (ptrId == mActivePointerId) { + final int newPointerIdx = ptrIdx == 0 ? 1 : 0; + mDownPos.set( + ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x), + ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y)); + mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx)); + mActivePointerId = ev.getPointerId(newPointerIdx); + } + break; + case MotionEvent.ACTION_MOVE: + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == INVALID_POINTER_ID) { + break; + } + mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos); + computeVelocity(mDir.getDisplacement(ev, pointerIndex, mLastPos), + ev.getEventTime()); + + // handle state and listener calls. + if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) { + setState(ScrollState.DRAGGING); + } + if (mState == ScrollState.DRAGGING) { + reportDragging(); + } + mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + // These are synthetic events and there is no need to update internal values. + if (mState == ScrollState.DRAGGING) { + setState(ScrollState.SETTLING); + } + break; + default: + break; + } + return true; + } + + public void finishedScrolling() { + setState(ScrollState.IDLE); + } + + private boolean reportDragStart(boolean recatch) { + mListener.onDragStart(!recatch); + if (DBG) { + Log.d(TAG, "onDragStart recatch:" + recatch); + } + return true; + } + + private void initializeDragging() { + if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { + mSubtractDisplacement = 0; + } + if (mDisplacement > 0) { + mSubtractDisplacement = mTouchSlop; + } else { + mSubtractDisplacement = -mTouchSlop; + } + } + + private boolean reportDragging() { + if (mDisplacement != mLastDisplacement) { + if (DBG) { + Log.d(TAG, String.format("onDrag disp=%.1f, velocity=%.1f", + mDisplacement, mVelocity)); + } + + mLastDisplacement = mDisplacement; + return mListener.onDrag(mDisplacement - mSubtractDisplacement, mVelocity); + } + return true; + } + + private void reportDragEnd() { + if (DBG) { + Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f", + mDisplacement, mVelocity)); + } + mListener.onDragEnd(mVelocity, Math.abs(mVelocity) > RELEASE_VELOCITY_PX_MS); + + } + + /** + * Computes the damped velocity. + */ + public float computeVelocity(float delta, long currentMillis) { + long previousMillis = mCurrentMillis; + mCurrentMillis = currentMillis; + + float deltaTimeMillis = mCurrentMillis - previousMillis; + float velocity = (deltaTimeMillis > 0) ? (delta / deltaTimeMillis) : 0; + if (Math.abs(mVelocity) < 0.001f) { + mVelocity = velocity; + } else { + float alpha = computeDampeningFactor(deltaTimeMillis); + mVelocity = interpolate(mVelocity, velocity, alpha); + } + return mVelocity; + } + + /** + * Returns a time-dependent dampening factor using delta time. + */ + private static float computeDampeningFactor(float deltaTime) { + return deltaTime / (SCROLL_VELOCITY_DAMPENING_RC + deltaTime); + } + + /** + * Returns the linear interpolation between two values + */ + private static float interpolate(float from, float to, float alpha) { + return (1.0f - alpha) * from + alpha * to; + } + + public static long calculateDuration(float velocity, float progressNeeded) { + // TODO: make these values constants after tuning. + float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity)); + float travelDistance = Math.max(0.2f, progressNeeded); + long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance); + if (DBG) { + Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded)); + } + return duration; + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/recents/SwipeUpOnboarding.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsOnboarding.java index b2472bf73874..89cc50970989 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/SwipeUpOnboarding.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsOnboarding.java @@ -16,13 +16,11 @@ package com.android.systemui.recents; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import android.annotation.TargetApi; import android.app.ActivityManager; -import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -31,6 +29,10 @@ import android.graphics.PorterDuff; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.RippleDrawable; import android.os.Build; +import android.os.SystemProperties; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -41,6 +43,7 @@ import android.view.animation.DecelerateInterpolator; import android.widget.ImageView; import android.widget.TextView; +import com.android.systemui.OverviewProxyService; import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.recents.misc.SysUiTaskStackChangeListener; @@ -50,9 +53,9 @@ import com.android.systemui.shared.system.ActivityManagerWrapper; * Shows onboarding for the new recents interaction in P (codenamed quickstep). */ @TargetApi(Build.VERSION_CODES.P) -public class SwipeUpOnboarding { +public class RecentsOnboarding { - private static final String TAG = "SwipeUpOnboarding"; + private static final String TAG = "RecentsOnboarding"; private static final boolean RESET_PREFS_FOR_DEBUG = false; private static final long SHOW_DELAY_MS = 500; private static final long SHOW_HIDE_DURATION_MS = 300; @@ -61,6 +64,7 @@ public class SwipeUpOnboarding { private final Context mContext; private final WindowManager mWindowManager; + private final OverviewProxyService mOverviewProxyService; private final View mLayout; private final TextView mTextView; private final ImageView mDismissView; @@ -113,11 +117,12 @@ public class SwipeUpOnboarding { } }; - public SwipeUpOnboarding(Context context) { + public RecentsOnboarding(Context context, OverviewProxyService overviewProxyService) { mContext = context; + mOverviewProxyService = overviewProxyService; final Resources res = context.getResources(); mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); - mLayout = LayoutInflater.from(mContext).inflate(R.layout.recents_swipe_up_onboarding, null); + mLayout = LayoutInflater.from(mContext).inflate(R.layout.recents_onboarding, null); mTextView = mLayout.findViewById(R.id.onboarding_text); mDismissView = mLayout.findViewById(R.id.dismiss); mDarkBackgroundColor = res.getColor(android.R.color.background_dark); @@ -135,25 +140,25 @@ public class SwipeUpOnboarding { mDismissView.setOnClickListener(v -> hide(true)); if (RESET_PREFS_FOR_DEBUG) { - Prefs.putBoolean(mContext, Prefs.Key.HAS_SWIPED_UP_FOR_RECENTS, false); + Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_RECENTS_ONBOARDING, false); Prefs.putInt(mContext, Prefs.Key.NUM_APPS_LAUNCHED, 0); } } public void onConnectedToLauncher() { - boolean alreadyLearnedSwipeUpForRecents = Prefs.getBoolean(mContext, - Prefs.Key.HAS_SWIPED_UP_FOR_RECENTS, false); - if (!mTaskListenerRegistered && !alreadyLearnedSwipeUpForRecents) { + boolean alreadySeenRecentsOnboarding = Prefs.getBoolean(mContext, + Prefs.Key.HAS_SEEN_RECENTS_ONBOARDING, false); + if (!mTaskListenerRegistered && !alreadySeenRecentsOnboarding) { ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskListener); mTaskListenerRegistered = true; } } public void onRecentsAnimationStarted() { - boolean alreadyLearnedSwipeUpForRecents = Prefs.getBoolean(mContext, - Prefs.Key.HAS_SWIPED_UP_FOR_RECENTS, false); - if (!alreadyLearnedSwipeUpForRecents) { - Prefs.putBoolean(mContext, Prefs.Key.HAS_SWIPED_UP_FOR_RECENTS, true); + boolean alreadySeenRecentsOnboarding = Prefs.getBoolean(mContext, + Prefs.Key.HAS_SEEN_RECENTS_ONBOARDING, false); + if (!alreadySeenRecentsOnboarding) { + Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_RECENTS_ONBOARDING, true); onDisconnectedFromLauncher(); } } @@ -173,6 +178,15 @@ public class SwipeUpOnboarding { } public void show() { + if (!shouldShow()) { + return; + } + CharSequence onboardingText = mOverviewProxyService.getOnboardingText(); + if (TextUtils.isEmpty(onboardingText)) { + Log.w(TAG, "Unable to get onboarding text"); + return; + } + mTextView.setText(onboardingText); // Only show in portrait. int orientation = mContext.getResources().getConfiguration().orientation; if (!mLayoutAttachedToWindow && orientation == Configuration.ORIENTATION_PORTRAIT) { @@ -196,6 +210,14 @@ public class SwipeUpOnboarding { } } + /** + * @return True unless setprop has been set to false, or we're in demo mode. + */ + private boolean shouldShow() { + return SystemProperties.getBoolean("persist.quickstep.onboarding.enabled", + !(mContext.getSystemService(UserManager.class)).isDemoUser()); + } + public void hide(boolean animate) { if (mLayoutAttachedToWindow) { if (animate) { @@ -208,6 +230,7 @@ public class SwipeUpOnboarding { .withEndAction(() -> mWindowManager.removeView(mLayout)) .start(); } else { + mLayout.animate().cancel(); mWindowManager.removeView(mLayout); } } @@ -239,7 +262,7 @@ public class SwipeUpOnboarding { flags, PixelFormat.TRANSLUCENT); lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; - lp.setTitle("SwipeUpOnboarding"); + lp.setTitle("RecentsOnboarding"); lp.gravity = Gravity.BOTTOM; return lp; } diff --git a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java index 57f7818eae58..3dd6e353c924 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java +++ b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java @@ -36,6 +36,7 @@ import android.view.accessibility.AccessibilityManager; import android.view.animation.DecelerateInterpolator; import android.widget.Button; import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -255,6 +256,11 @@ public class ScreenPinningRequest implements View.OnClickListener { : R.string.screen_pinning_description_recents_invisible; } + ((ImageView) mLayout.findViewById(R.id.screen_pinning_back_icon)) + .setImageDrawable(navigationBarView.getBackDrawable(mContext)); + ((ImageView) mLayout.findViewById(R.id.screen_pinning_home_icon)) + .setImageDrawable(navigationBarView.getHomeDrawable(mContext)); + ((TextView) mLayout.findViewById(R.id.screen_pinning_description)) .setText(descriptionStringResId); final int backBgVisibility = touchExplorationEnabled ? View.INVISIBLE : View.VISIBLE; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index bf4a225a00ec..2acb1bb2613b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -38,10 +38,12 @@ import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.Picture; import android.graphics.PixelFormat; import android.graphics.PointF; import android.graphics.Rect; @@ -57,13 +59,10 @@ import android.provider.MediaStore; import android.util.DisplayMetrics; import android.util.Slog; import android.view.Display; -import android.view.DisplayListCanvas; import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.RenderNode; import android.view.Surface; import android.view.SurfaceControl; -import android.view.ThreadedRenderer; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; @@ -233,14 +232,12 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { */ private Bitmap generateAdjustedHwBitmap(Bitmap bitmap, int width, int height, Matrix matrix, Paint paint, int color) { - RenderNode node = RenderNode.create("ScreenshotCanvas", null); - node.setLeftTopRightBottom(0, 0, width, height); - node.setClipToBounds(false); - DisplayListCanvas canvas = node.start(width, height); + Picture picture = new Picture(); + Canvas canvas = picture.beginRecording(width, height); canvas.drawColor(color); canvas.drawBitmap(bitmap, matrix, paint); - node.end(canvas); - return ThreadedRenderer.createHardwareBitmap(node, width, height); + picture.endRecording(); + return Bitmap.createBitmap(picture); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java index eb5619b11487..0876507465a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java @@ -308,6 +308,10 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } } + public void setRippleAllowed(boolean allowed) { + mBackgroundNormal.setPressedAllowed(allowed); + } + private boolean handleTouchEventDimmed(MotionEvent event) { if (mNeedsDimming && !mDimmed) { // We're actually dimmed, but our content isn't dimmable, let's ensure we have a ripple diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 11bdf6b3c72e..fa177f2bb26c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -156,7 +156,7 @@ public class CommandQueue extends IStatusBar.Stub { default void handleShowGlobalActionsMenu() { } default void handleShowShutdownUi(boolean isReboot, String reason) { } - default void showChargingAnimation(int batteryLevel) { } + default void showWirelessChargingAnimation(int batteryLevel) { } default void onRotationProposal(int rotation, boolean isValid) { } @@ -497,7 +497,7 @@ public class CommandQueue extends IStatusBar.Stub { } @Override - public void showChargingAnimation(int batteryLevel) { + public void showWirelessChargingAnimation(int batteryLevel) { mHandler.removeMessages(MSG_SHOW_CHARGING_ANIMATION); mHandler.obtainMessage(MSG_SHOW_CHARGING_ANIMATION, batteryLevel, 0) .sendToTarget(); @@ -784,7 +784,7 @@ public class CommandQueue extends IStatusBar.Stub { break; case MSG_SHOW_CHARGING_ANIMATION: for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).showChargingAnimation(msg.arg1); + mCallbacks.get(i).showWirelessChargingAnimation(msg.arg1); } break; case MSG_SHOW_PINNING_TOAST_ENTER_EXIT: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java index b3f68d357083..2723df73aa52 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java @@ -370,14 +370,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mNotificationInflater.inflateNotificationViews(); } - @Override - public void setPressed(boolean pressed) { - if (isOnKeyguard() || mEntry.notification.getNotification().contentIntent == null) { - // We're dropping the ripple if we have a collapse / launch animation - super.setPressed(pressed); - } - } - public void onNotificationUpdated() { for (NotificationContentView l : mLayouts) { l.onNotificationUpdated(mEntry); @@ -407,6 +399,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView showBlockingHelper(mEntry.userSentiment == NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE); + updateRippleAllowed(); } @VisibleForTesting @@ -1805,6 +1798,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf); } } + updateRippleAllowed(); + } + + private void updateRippleAllowed() { + boolean allowed = isOnKeyguard() + || mEntry.notification.getNotification().contentIntent == null; + setRippleAllowed(allowed); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index b7a15005b170..22e8909b4812 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -300,34 +300,10 @@ public class KeyguardIndicationController { } else if (mPowerPluggedIn) { String indication = computePowerIndication(); if (animate) { - int yTranslation = mContext.getResources().getInteger( - R.integer.wired_charging_aod_text_animation_distance); - int animateUpDuration = mContext.getResources().getInteger( - R.integer.wired_charging_aod_text_animation_duration_up); - int animateDownDuration = mContext.getResources().getInteger( - R.integer.wired_charging_aod_text_animation_duration_down); - mTextView.animate() - .translationYBy(yTranslation) - .setInterpolator(Interpolators.LINEAR) - .setDuration(animateUpDuration) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - mTextView.switchIndication(indication); - } - @Override - public void onAnimationEnd(Animator animation) { - mTextView.animate() - .setDuration(animateDownDuration) - .setInterpolator(Interpolators.BOUNCE) - .translationYBy(-1 * yTranslation) - .setListener(null); - } - }); + animateText(mTextView, indication); } else { mTextView.switchIndication(indication); } - } else { String percentage = NumberFormat.getPercentInstance() .format(mBatteryLevel / 100f); @@ -355,8 +331,12 @@ public class KeyguardIndicationController { if (DEBUG_CHARGING_SPEED) { indication += ", " + (mChargingWattage / 1000) + " mW"; } - mTextView.switchIndication(indication); mTextView.setTextColor(mInitialTextColor); + if (animate) { + animateText(mTextView, indication); + } else { + mTextView.switchIndication(indication); + } } else if (!TextUtils.isEmpty(trustManagedIndication) && updateMonitor.getUserTrustIsManaged(userId) && !updateMonitor.getUserHasTrust(userId)) { @@ -369,6 +349,34 @@ public class KeyguardIndicationController { } } + // animates textView - textView moves up and bounces down + private void animateText(KeyguardIndicationTextView textView, String indication) { + int yTranslation = mContext.getResources().getInteger( + R.integer.wired_charging_keyguard_text_animation_distance); + int animateUpDuration = mContext.getResources().getInteger( + R.integer.wired_charging_keyguard_text_animation_duration_up); + int animateDownDuration = mContext.getResources().getInteger( + R.integer.wired_charging_keyguard_text_animation_duration_down); + textView.animate() + .translationYBy(yTranslation) + .setInterpolator(Interpolators.LINEAR) + .setDuration(animateUpDuration) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + textView.switchIndication(indication); + } + @Override + public void onAnimationEnd(Animator animation) { + textView.animate() + .setDuration(animateDownDuration) + .setInterpolator(Interpolators.BOUNCE) + .translationYBy(-1 * yTranslation) + .setListener(null); + } + }); + } + private String computePowerIndication() { if (mPowerCharged) { return mContext.getResources().getString(R.string.keyguard_charged); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java index d6beb7fb2699..0ff4dde580b7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java @@ -28,6 +28,7 @@ import android.graphics.drawable.RippleDrawable; import android.util.AttributeSet; import android.view.View; +import com.android.internal.util.ArrayUtils; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.statusbar.notification.ActivityLaunchAnimator; @@ -50,6 +51,7 @@ public class NotificationBackgroundView extends View { private boolean mExpandAnimationRunning; private float mActualWidth; private int mDrawableAlpha = 255; + private boolean mIsPressedAllowed; public NotificationBackgroundView(Context context, AttributeSet attrs) { super(context, attrs); @@ -94,13 +96,7 @@ public class NotificationBackgroundView extends View { @Override protected void drawableStateChanged() { - drawableStateChanged(mBackground); - } - - private void drawableStateChanged(Drawable d) { - if (d != null && d.isStateful()) { - d.setState(getDrawableState()); - } + setState(getDrawableState()); } @Override @@ -177,7 +173,13 @@ public class NotificationBackgroundView extends View { } public void setState(int[] drawableState) { - mBackground.setState(drawableState); + if (mBackground != null && mBackground.isStateful()) { + if (!mIsPressedAllowed) { + drawableState = ArrayUtils.removeInt(drawableState, + com.android.internal.R.attr.state_pressed); + } + mBackground.setState(drawableState); + } } public void setRippleColor(int color) { @@ -249,10 +251,17 @@ public class NotificationBackgroundView extends View { (GradientDrawable) ((LayerDrawable) mBackground).getDrawable(0); gradientDrawable.setXfermode( running ? new PorterDuffXfermode(PorterDuff.Mode.SRC) : null); + // Speed optimization: disable AA if transfer mode is not SRC_OVER. AA is not easy to + // spot during animation anyways. + gradientDrawable.setAntiAlias(!running); } if (!mExpandAnimationRunning) { setDrawableAlpha(mDrawableAlpha); } invalidate(); } + + public void setPressedAllowed(boolean allowed) { + mIsPressedAllowed = allowed; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java index e811ed3d77ae..c4d0b79a69c8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java @@ -22,7 +22,6 @@ import android.app.RemoteInput; import android.content.Context; import android.graphics.Rect; import android.os.Build; -import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.util.AttributeSet; import android.util.Log; @@ -36,6 +35,7 @@ import android.widget.LinearLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.NotificationColorUtil; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.statusbar.notification.HybridGroupManager; import com.android.systemui.statusbar.notification.HybridNotificationView; @@ -44,6 +44,7 @@ import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.NotificationViewWrapper; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.policy.RemoteInputView; +import com.android.systemui.statusbar.policy.SmartReplyConstants; import com.android.systemui.statusbar.policy.SmartReplyView; /** @@ -75,6 +76,8 @@ public class NotificationContentView extends FrameLayout { private RemoteInputView mExpandedRemoteInput; private RemoteInputView mHeadsUpRemoteInput; + + private SmartReplyConstants mSmartReplyConstants; private SmartReplyView mExpandedSmartReplyView; private NotificationViewWrapper mContractedWrapper; @@ -145,6 +148,7 @@ public class NotificationContentView extends FrameLayout { public NotificationContentView(Context context, AttributeSet attrs) { super(context, attrs); mHybridGroupManager = new HybridGroupManager(getContext(), this); + mSmartReplyConstants = Dependency.get(SmartReplyConstants.class); initView(); } @@ -1166,8 +1170,7 @@ public class NotificationContentView extends FrameLayout { return; } - boolean enableSmartReplies = Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.ENABLE_SMART_REPLIES_IN_NOTIFICATIONS, 0) != 0; + boolean enableSmartReplies = mSmartReplyConstants.isEnabled(); boolean hasRemoteInput = false; RemoteInput remoteInputWithChoices = null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGutsManager.java index 1aaa3b2593d2..f730601ac919 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGutsManager.java @@ -24,6 +24,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.net.Uri; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -131,12 +132,13 @@ public class NotificationGutsManager implements Dumpable { } /** - * Sends an intent to open the notification settings for a particular package and optional + * Sends an intent to open the app settings for a particular package and optional * channel. */ private void startAppNotificationSettingsActivity(String packageName, final int appUid, final NotificationChannel channel, ExpandableNotificationRow row) { - final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", packageName, null)); intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName); intent.putExtra(Settings.EXTRA_APP_UID, appUid); if (channel != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInfo.java index 735f4fd82dec..afe906c28ea5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInfo.java @@ -50,7 +50,6 @@ import com.android.settingslib.Utils; import com.android.systemui.Interpolators; import com.android.systemui.R; -import java.lang.IllegalArgumentException; import java.util.List; import java.util.Set; @@ -274,7 +273,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G } private void saveImportance() { - if (mNonblockable || !hasImportanceChanged()) { + if (mNonblockable) { return; } MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE, @@ -409,7 +408,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G final int centerY = v.getHeight() / 2; final int x = targetLoc[0] - parentLoc[0] + centerX; final int y = targetLoc[1] - parentLoc[1] + centerY; - mGutsContainer.closeControls(x, y, false /* save */, false /* force */); + mGutsContainer.closeControls(x, y, true /* save */, false /* force */); } @Override @@ -429,7 +428,9 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G @Override public boolean handleCloseControls(boolean save, boolean force) { - if (save && hasImportanceChanged()) { + // Save regardless of the importance so we can lock the importance field if the user wants + // to keep getting notifications + if (save) { if (mCheckSaveListener != null) { mCheckSaveListener.checkSave(this::saveImportance, mSbn); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index f25379ab0b22..3c480d80dea8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -299,6 +299,11 @@ public class NotificationRemoteInputManager implements Dumpable { } }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY); } + try { + mBarService.onNotificationDirectReplied(entry.notification.getKey()); + } catch (RemoteException e) { + // Nothing to do, system going down + } } }); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java index 46b218f0de6b..13b13fd943f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java @@ -38,9 +38,9 @@ import android.view.accessibility.AccessibilityEvent; import android.widget.ImageView; import android.widget.LinearLayout; +import com.android.settingslib.graph.SignalDrawable; import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.statusbar.phone.SignalDrawable; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.policy.DarkIconDispatcher; import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarFacetButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarFacetButton.java new file mode 100644 index 000000000000..53101a5bd61f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarFacetButton.java @@ -0,0 +1,161 @@ +package com.android.systemui.statusbar.car; + +import android.content.Context; +import android.content.Intent; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.android.keyguard.AlphaOptimizedImageButton; +import com.android.systemui.R; + +/** + * CarFacetButton is a ui component designed to be used as a shortcut for an app of a defined + * category. It can also render a indicator impling that there are more options of apps to launch + * using this component. This is done with a "More icon" currently an arrow as defined in the layout + * file. The class is to serve as an example. + * Usage example: A button that allows a user to select a music app and indicate that there are + * other music apps installed. + */ +public class CarFacetButton extends LinearLayout { + private static final float SELECTED_ALPHA = 1f; + private static final float UNSELECTED_ALPHA = 0.7f; + + private static final String FACET_FILTER_DELIMITER = ";"; + /** + * Extra information to be sent to a helper to make the decision of what app to launch when + * clicked. + */ + private static final String EXTRA_FACET_CATEGORIES = "categories"; + private static final String EXTRA_FACET_PACKAGES = "packages"; + private static final String EXTRA_FACET_ID = "filter_id"; + private static final String EXTRA_FACET_LAUNCH_PICKER = "launch_picker"; + + private Context mContext; + private AlphaOptimizedImageButton mIcon; + private AlphaOptimizedImageButton mMoreIcon; + private boolean mSelected = false; + /** App categories that are to be used with this widget */ + private String[] mFacetCategories; + /** App packages that are allowed to be used with this widget */ + private String[] mFacetPackages; + + + public CarFacetButton(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + View.inflate(context, R.layout.car_facet_button, this); + + // extract custom attributes + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CarFacetButton); + setupIntents(typedArray); + setupIcons(typedArray); + } + + /** + * Reads the custom attributes to setup click handlers for this component. + */ + private void setupIntents(TypedArray typedArray) { + String intentString = typedArray.getString(R.styleable.CarFacetButton_intent); + String longPressIntentString = typedArray.getString(R.styleable.CarFacetButton_longIntent); + String categoryString = typedArray.getString(R.styleable.CarFacetButton_categories); + String packageString = typedArray.getString(R.styleable.CarFacetButton_packages); + try { + final Intent intent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME); + intent.putExtra(EXTRA_FACET_ID, Integer.toString(getId())); + + if (packageString != null) { + mFacetPackages = packageString.split(FACET_FILTER_DELIMITER); + intent.putExtra(EXTRA_FACET_PACKAGES, mFacetPackages); + } + if (categoryString != null) { + mFacetCategories = categoryString.split(FACET_FILTER_DELIMITER); + intent.putExtra(EXTRA_FACET_CATEGORIES, mFacetCategories); + } + + setOnClickListener(v -> { + intent.putExtra(EXTRA_FACET_LAUNCH_PICKER, mSelected); + mContext.startActivity(intent); + }); + + if (longPressIntentString != null) { + final Intent longPressIntent = Intent.parseUri(longPressIntentString, + Intent.URI_INTENT_SCHEME); + setOnLongClickListener(v -> { + mContext.startActivity(longPressIntent); + return true; + }); + } + } catch (Exception e) { + throw new RuntimeException("Failed to attach intent", e); + } + } + + + private void setupIcons(TypedArray styledAttributes) { + mIcon = findViewById(R.id.car_nav_button_icon); + mIcon.setScaleType(ImageView.ScaleType.CENTER); + mIcon.setClickable(false); + mIcon.setAlpha(UNSELECTED_ALPHA); + int iconResourceId = styledAttributes.getResourceId(R.styleable.CarFacetButton_icon, 0); + if (iconResourceId == 0) { + throw new RuntimeException("specified icon resource was not found and is required"); + } + mIcon.setImageResource(iconResourceId); + + mMoreIcon = findViewById(R.id.car_nav_button_more_icon); + mMoreIcon.setClickable(false); + mMoreIcon.setImageDrawable(getContext().getDrawable(R.drawable.car_ic_arrow)); + mMoreIcon.setAlpha(UNSELECTED_ALPHA); + mMoreIcon.setVisibility(GONE); + } + + /** + * @return The app categories the component represents + */ + public String[] getCategories() { + if (mFacetCategories == null) { + return new String[0]; + } + return mFacetCategories; + } + + /** + * @return The valid packages that should be considered. + */ + public String[] getFacetPackages() { + if (mFacetPackages == null) { + return new String[0]; + } + return mFacetPackages; + } + + /** + * Updates the alpha of the icons to "selected" and shows the "More icon" + * @param selected true if the view must be selected, false otherwise + */ + public void setSelected(boolean selected) { + super.setSelected(selected); + setSelected(selected, selected); + } + + /** + * Updates the visual state to let the user know if it's been selected. + * @param selected true if should update the alpha of the icon to selected, false otherwise + * @param showMoreIcon true if the "more icon" should be shown, false otherwise + */ + public void setSelected(boolean selected, boolean showMoreIcon) { + mSelected = selected; + if (selected) { + mMoreIcon.setVisibility(showMoreIcon ? VISIBLE : GONE); + mMoreIcon.setAlpha(SELECTED_ALPHA); + mIcon.setAlpha(SELECTED_ALPHA); + } else { + mMoreIcon.setVisibility(GONE); + mIcon.setAlpha(UNSELECTED_ALPHA); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarFacetButtonController.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarFacetButtonController.java new file mode 100644 index 000000000000..e8c9a5e5693a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarFacetButtonController.java @@ -0,0 +1,114 @@ +package com.android.systemui.statusbar.car; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.view.View; +import android.view.ViewGroup; + +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +/** + * CarFacetButtons placed on the nav bar are designed to have visual indication that the active + * application on screen is associated with it. This is basically a similar concept to a radio + * button group. + */ +public class CarFacetButtonController { + + protected HashMap<String, CarFacetButton> mButtonsByCategory = new HashMap<>(); + protected HashMap<String, CarFacetButton> mButtonsByPackage = new HashMap<>(); + protected CarFacetButton mSelectedFacetButton; + protected Context mContext; + + public CarFacetButtonController(Context context) { + mContext = context; + } + + /** + * Goes through the supplied CarNavigationBarView and keeps track of all the CarFacetButtons + * such that it can select and unselect them based on running task chages + * @param bar that may contain CarFacetButtons + */ + public void addCarNavigationBar(CarNavigationBarView bar) { + findFacets(bar); + } + + private void findFacets(ViewGroup root) { + final int childCount = root.getChildCount(); + + for (int i = 0; i < childCount; ++i) { + final View v = root.getChildAt(i); + if (v instanceof CarFacetButton) { + CarFacetButton facetButton = (CarFacetButton) v; + String[] categories = facetButton.getCategories(); + for (int j = 0; j < categories.length; j++) { + String category = categories[j]; + mButtonsByCategory.put(category, facetButton); + } + + String[] facetPackages = facetButton.getFacetPackages(); + for (int j = 0; j < facetPackages.length; j++) { + String facetPackage = facetPackages[j]; + mButtonsByPackage.put(facetPackage, facetButton); + } + } else if (v instanceof ViewGroup) { + findFacets((ViewGroup) v); + } + } + } + + + /** + * This will unselect the currently selected CarFacetButton and determine which one should be + * selected next. It does this by reading the properties on the CarFacetButton and seeing if + * they are a match with the supplied taskino. + * @param taskInfo of the currently running application + */ + public void taskChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (taskInfo == null || taskInfo.baseActivity == null) { + return; + } + String packageName = taskInfo.baseActivity.getPackageName(); + + // If the package name belongs to a filter, then highlight appropriate button in + // the navigation bar. + if (mSelectedFacetButton != null) { + mSelectedFacetButton.setSelected(false); + } + CarFacetButton facetButton = mButtonsByPackage.get(packageName); + if (facetButton != null) { + facetButton.setSelected(true); + mSelectedFacetButton = facetButton; + } else { + String category = getPackageCategory(packageName); + if (category != null) { + facetButton = mButtonsByCategory.get(category); + facetButton.setSelected(true); + mSelectedFacetButton = facetButton; + } + } + } + + protected String getPackageCategory(String packageName) { + PackageManager pm = mContext.getPackageManager(); + Set<String> supportedCategories = mButtonsByCategory.keySet(); + for (String category : supportedCategories) { + Intent intent = new Intent(); + intent.setPackage(packageName); + intent.setAction(Intent.ACTION_MAIN); + intent.addCategory(category); + List<ResolveInfo> list = pm.queryIntentActivities(intent, 0); + if (list.size() > 0) { + // Cache this package name into facetPackageMap, so we won't have to query + // all categories next time this package name shows up. + mButtonsByPackage.put(packageName, mButtonsByCategory.get(category)); + return category; + } + } + return null; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarController.java deleted file mode 100644 index 64c52ed6d29f..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarController.java +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.statusbar.car; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - -import android.app.ActivityManager; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.support.v4.util.SimpleArrayMap; -import android.text.TextUtils; -import android.util.Log; -import android.util.SparseBooleanArray; -import android.view.View; -import android.widget.LinearLayout; -import com.android.systemui.R; - -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; - -/** - * A controller to populate data for CarNavigationBarView and handle user interactions. - * - * <p>Each button inside the navigation bar is defined by data in arrays_car.xml. OEMs can - * customize the navigation buttons by updating arrays_car.xml appropriately in an overlay. - */ -class CarNavigationBarController { - private static final String TAG = "CarNavBarController"; - - private static final String EXTRA_FACET_CATEGORIES = "categories"; - private static final String EXTRA_FACET_PACKAGES = "packages"; - private static final String EXTRA_FACET_ID = "filter_id"; - private static final String EXTRA_FACET_LAUNCH_PICKER = "launch_picker"; - - /** - * Each facet of the navigation bar maps to a set of package names or categories defined in - * arrays_car.xml. Package names for a given facet are delimited by ";". - */ - private static final String FACET_FILTER_DELIMITER = ";"; - - private final Context mContext; - private final CarNavigationBarView mNavBar; - private final CarStatusBar mStatusBar; - - /** - * Set of categories each facet will filter on. - */ - private final List<String[]> mFacetCategories = new ArrayList<>(); - - /** - * Set of package names each facet will filter on. - */ - private final List<String[]> mFacetPackages = new ArrayList<>(); - - private final SimpleArrayMap<String, Integer> mFacetCategoryMap = new SimpleArrayMap<>(); - private final SimpleArrayMap<String, Integer> mFacetPackageMap = new SimpleArrayMap<>(); - - private final List<CarNavigationButton> mNavButtons = new ArrayList<>(); - - private final SparseBooleanArray mFacetHasMultipleAppsCache = new SparseBooleanArray(); - - private int mCurrentFacetIndex; - private Intent mPersistentTaskIntent; - - public CarNavigationBarController(Context context, CarNavigationBarView navBar, - CarStatusBar activityStarter) { - mContext = context; - mNavBar = navBar; - mStatusBar = activityStarter; - bind(); - - if (context.getResources().getBoolean(R.bool.config_enablePersistentDockedActivity)) { - setupPersistentDockedTask(); - } - } - - private void setupPersistentDockedTask() { - try { - mPersistentTaskIntent = Intent.parseUri( - mContext.getString(R.string.config_persistentDockedActivityIntentUri), - Intent.URI_INTENT_SCHEME); - } catch (URISyntaxException e) { - Log.e(TAG, "Malformed persistent task intent."); - } - } - - public void taskChanged(String packageName, ActivityManager.RunningTaskInfo taskInfo) { - // If the package name belongs to a filter, then highlight appropriate button in - // the navigation bar. - if (mFacetPackageMap.containsKey(packageName)) { - setCurrentFacet(mFacetPackageMap.get(packageName)); - } - - // Check if the package matches any of the categories for the facets - String category = getPackageCategory(packageName); - if (category != null) { - setCurrentFacet(mFacetCategoryMap.get(category)); - } - - // Set up the persistent docked task if needed. - boolean isHomeTask = - taskInfo.configuration.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME; - if (mPersistentTaskIntent != null && !mStatusBar.hasDockedTask() && !isHomeTask) { - mStatusBar.startActivityOnStack(mPersistentTaskIntent, - WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_UNDEFINED); - } - } - - public void onPackageChange(String packageName) { - if (mFacetPackageMap.containsKey(packageName)) { - int index = mFacetPackageMap.get(packageName); - mFacetHasMultipleAppsCache.put(index, facetHasMultiplePackages(index)); - // No need to check categories because we've already refreshed the cache. - return; - } - - String category = getPackageCategory(packageName); - if (mFacetCategoryMap.containsKey(category)) { - int index = mFacetCategoryMap.get(category); - mFacetHasMultipleAppsCache.put(index, facetHasMultiplePackages(index)); - } - } - - /** - * Iterates through the items in arrays_car.xml and sets up the facet bar buttons to - * perform the task in that configuration file when clicked or long-pressed. - */ - private void bind() { - Resources res = mContext.getResources(); - - TypedArray icons = res.obtainTypedArray(R.array.car_facet_icons); - TypedArray intents = res.obtainTypedArray(R.array.car_facet_intent_uris); - TypedArray longPressIntents = res.obtainTypedArray(R.array.car_facet_longpress_intent_uris); - TypedArray facetPackageNames = res.obtainTypedArray(R.array.car_facet_package_filters); - TypedArray facetCategories = res.obtainTypedArray(R.array.car_facet_category_filters); - - try { - if (icons.length() != intents.length() - || icons.length() != longPressIntents.length() - || icons.length() != facetPackageNames.length() - || icons.length() != facetCategories.length()) { - throw new RuntimeException("car_facet array lengths do not match"); - } - - for (int i = 0, size = icons.length(); i < size; i++) { - Drawable icon = icons.getDrawable(i); - CarNavigationButton button = createNavButton(icon); - initClickListeners(button, i, intents.getString(i), longPressIntents.getString(i)); - - mNavButtons.add(button); - mNavBar.addButton(button, createNavButton(icon) /* lightsOutButton */); - - initFacetFilterMaps(i, facetPackageNames.getString(i).split(FACET_FILTER_DELIMITER), - facetCategories.getString(i).split(FACET_FILTER_DELIMITER)); - mFacetHasMultipleAppsCache.put(i, facetHasMultiplePackages(i)); - } - } finally { - // Clean up all the TypedArrays. - icons.recycle(); - intents.recycle(); - longPressIntents.recycle(); - facetPackageNames.recycle(); - facetCategories.recycle(); - } - } - - /** - * Recreates each of the buttons on a density or font scale change. This manual process is - * necessary since this class is not part of an activity that automatically gets recreated. - */ - public void onDensityOrFontScaleChanged() { - TypedArray icons = mContext.getResources().obtainTypedArray(R.array.car_facet_icons); - - try { - int length = icons.length(); - if (length != mNavButtons.size()) { - // This should not happen since the mNavButtons list is created from the length - // of the icons array in bind(). - throw new RuntimeException("car_facet array lengths do not match number of " - + "created buttons."); - } - - for (int i = 0; i < length; i++) { - Drawable icon = icons.getDrawable(i); - - // Setting a new icon will trigger a requestLayout() call if necessary. - mNavButtons.get(i).setResources(icon); - } - } finally { - icons.recycle(); - } - } - - private void initFacetFilterMaps(int id, String[] packageNames, String[] categories) { - mFacetCategories.add(categories); - for (String category : categories) { - mFacetCategoryMap.put(category, id); - } - - mFacetPackages.add(packageNames); - for (String packageName : packageNames) { - mFacetPackageMap.put(packageName, id); - } - } - - private String getPackageCategory(String packageName) { - PackageManager pm = mContext.getPackageManager(); - int size = mFacetCategories.size(); - // For each facet, check if the given package name matches one of its categories - for (int i = 0; i < size; i++) { - String[] categories = mFacetCategories.get(i); - for (int j = 0; j < categories.length; j++) { - String category = categories[j]; - Intent intent = new Intent(); - intent.setPackage(packageName); - intent.setAction(Intent.ACTION_MAIN); - intent.addCategory(category); - List<ResolveInfo> list = pm.queryIntentActivities(intent, 0); - if (list.size() > 0) { - // Cache this package name into facetPackageMap, so we won't have to query - // all categories next time this package name shows up. - mFacetPackageMap.put(packageName, mFacetCategoryMap.get(category)); - return category; - } - } - } - return null; - } - - /** - * Helper method to check if a given facet has multiple packages associated with it. This can - * be resource defined package names or package names filtered by facet category. - * - * @return {@code true} if the facet at the given index has more than one package. - */ - private boolean facetHasMultiplePackages(int index) { - PackageManager pm = mContext.getPackageManager(); - - // Check if the packages defined for the filter actually exists on the device - String[] packages = mFacetPackages.get(index); - if (packages.length > 1) { - int count = 0; - for (int i = 0; i < packages.length; i++) { - count += pm.getLaunchIntentForPackage(packages[i]) != null ? 1 : 0; - if (count > 1) { - return true; - } - } - } - - // If there weren't multiple packages defined for the facet, check the categories - // and see if they resolve to multiple package names - String categories[] = mFacetCategories.get(index); - - int count = 0; - for (int i = 0; i < categories.length; i++) { - String category = categories[i]; - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_MAIN); - intent.addCategory(category); - count += pm.queryIntentActivities(intent, 0).size(); - if (count > 1) { - return true; - } - } - return false; - } - - /** - * Sets the facet at the given index to be the facet that is currently active. The button will - * be highlighted appropriately. - */ - private void setCurrentFacet(int index) { - if (index == mCurrentFacetIndex) { - return; - } - - if (mNavButtons.get(mCurrentFacetIndex) != null) { - mNavButtons.get(mCurrentFacetIndex) - .setSelected(false /* selected */, false /* showMoreIcon */); - } - - if (mNavButtons.get(index) != null) { - mNavButtons.get(index).setSelected(true /* selected */, - mFacetHasMultipleAppsCache.get(index) /* showMoreIcon */); - } - - mCurrentFacetIndex = index; - } - - /** - * Creates the View that is used for the buttons along the navigation bar. - * - * @param icon The icon to be used for the button. - */ - private CarNavigationButton createNavButton(Drawable icon) { - CarNavigationButton button = (CarNavigationButton) View.inflate(mContext, - R.layout.car_navigation_button, null); - button.setResources(icon); - LinearLayout.LayoutParams lp = - new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1); - button.setLayoutParams(lp); - - return button; - } - - /** - * Initializes the click and long click listeners that correspond to the given command string. - * The click listeners are attached to the given button. - */ - private void initClickListeners(View button, int index, String clickString, - String longPressString) { - // Each button at least have an action when pressed. - if (TextUtils.isEmpty(clickString)) { - throw new RuntimeException("Facet at index " + index + " does not have click action."); - } - - try { - Intent intent = Intent.parseUri(clickString, Intent.URI_INTENT_SCHEME); - button.setOnClickListener(v -> onFacetClicked(intent, index)); - } catch (URISyntaxException e) { - throw new RuntimeException("Malformed intent uri", e); - } - - if (TextUtils.isEmpty(longPressString)) { - button.setLongClickable(false); - return; - } - - try { - Intent intent = Intent.parseUri(longPressString, Intent.URI_INTENT_SCHEME); - button.setOnLongClickListener(v -> { - onFacetLongClicked(intent, index); - return true; - }); - } catch (URISyntaxException e) { - throw new RuntimeException("Malformed long-press intent uri", e); - } - } - - /** - * Handles a click on a facet. A click will trigger the given Intent. - * - * @param index The index of the facet that was clicked. - */ - private void onFacetClicked(Intent intent, int index) { - String packageName = intent.getPackage(); - - if (packageName == null && !intent.getCategories().contains(Intent.CATEGORY_HOME)) { - return; - } - - intent.putExtra(EXTRA_FACET_CATEGORIES, mFacetCategories.get(index)); - intent.putExtra(EXTRA_FACET_PACKAGES, mFacetPackages.get(index)); - // The facet is identified by the index in which it was added to the nav bar. - // This value can be used to determine which facet was selected - intent.putExtra(EXTRA_FACET_ID, Integer.toString(index)); - - // If the current facet is clicked, we want to launch the picker by default - // rather than the "preferred/last run" app. - intent.putExtra(EXTRA_FACET_LAUNCH_PICKER, index == mCurrentFacetIndex); - - int windowingMode = WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY; - int activityType = ACTIVITY_TYPE_UNDEFINED; - if (intent.getCategories().contains(Intent.CATEGORY_HOME)) { - windowingMode = WINDOWING_MODE_UNDEFINED; - activityType = ACTIVITY_TYPE_HOME; - } - - setCurrentFacet(index); - mStatusBar.startActivityOnStack(intent, windowingMode, activityType); - } - - /** - * Handles a long-press on a facet. The long-press will trigger the given Intent. - * - * @param index The index of the facet that was clicked. - */ - private void onFacetLongClicked(Intent intent, int index) { - setCurrentFacet(index); - mStatusBar.startActivityOnStack(intent, - WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_UNDEFINED); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarView.java index e5a311d099d5..1d9ef616d98d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationBarView.java @@ -16,17 +16,15 @@ package com.android.systemui.statusbar.car; +import android.app.UiModeManager; import android.content.Context; -import android.graphics.Canvas; import android.util.AttributeSet; -import android.view.MotionEvent; +import android.util.Log; import android.view.View; import android.widget.LinearLayout; +import com.android.keyguard.AlphaOptimizedImageButton; import com.android.systemui.R; -import com.android.systemui.plugins.statusbar.phone.NavGesture; -import com.android.systemui.statusbar.phone.NavigationBarGestureHelper; -import com.android.systemui.statusbar.phone.NavigationBarView; /** * A custom navigation bar for the automotive use case. @@ -34,9 +32,10 @@ import com.android.systemui.statusbar.phone.NavigationBarView; * The navigation bar in the automotive use case is more like a list of shortcuts, rendered * in a linear layout. */ -class CarNavigationBarView extends NavigationBarView { +class CarNavigationBarView extends LinearLayout { private LinearLayout mNavButtons; - private LinearLayout mLightsOutButtons; + private AlphaOptimizedImageButton mNotificationsButton; + private CarStatusBar mCarStatusBar; public CarNavigationBarView(Context context, AttributeSet attrs) { super(context, attrs); @@ -45,99 +44,16 @@ class CarNavigationBarView extends NavigationBarView { @Override public void onFinishInflate() { mNavButtons = findViewById(R.id.nav_buttons); - mLightsOutButtons = findViewById(R.id.lights_out); - } - public void addButton(CarNavigationButton button, CarNavigationButton lightsOutButton){ - mNavButtons.addView(button); - mLightsOutButtons.addView(lightsOutButton); + mNotificationsButton = findViewById(R.id.notifications); + mNotificationsButton.setOnClickListener(this::onNotificationsClick); } - @Override - public void setDisabledFlags(int disabledFlags, boolean force) { - // TODO: Populate. + void setStatusBar(CarStatusBar carStatusBar) { + mCarStatusBar = carStatusBar; } - @Override - public void reorient() { - // We expect the car head unit to always have a fixed rotation so we ignore this. The super - // class implentation expects mRotatedViews to be populated, so if you call into it, there - // is a possibility of a NullPointerException. - } - - @Override - public View getCurrentView() { - return this; - } - - @Override - public void setNavigationIconHints(int hints, boolean force) { - // We do not need to set the navigation icon hints for a vehicle - // Calling setNavigationIconHints in the base class will result in a NPE as the car - // navigation bar does not have a back button. - } - - @Override - public void onPluginConnected(NavGesture plugin, Context context) { - // set to null version of the plugin ignoring incoming arg. - super.onPluginConnected(new NullNavGesture(), context); - } - - @Override - public void onPluginDisconnected(NavGesture plugin) { - // reinstall the null nav gesture plugin - super.onPluginConnected(new NullNavGesture(), getContext()); - } - - /** - * Null object pattern to work around expectations of the base class. - * This is a temporary solution to have the car system ui working. - * Already underway is a refactor of they car sys ui as to not use this class - * hierarchy. - */ - private static class NullNavGesture implements NavGesture { - @Override - public GestureHelper getGestureHelper() { - return new GestureHelper() { - @Override - public boolean onTouchEvent(MotionEvent event) { - return false; - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - return false; - } - - @Override - public void setBarState(boolean vertical, boolean isRtl) { - } - - @Override - public void onDraw(Canvas canvas) { - } - - @Override - public void onDarkIntensityChange(float intensity) { - } - - @Override - public void onLayout(boolean changed, int left, int top, int right, int bottom) { - } - }; - } - - @Override - public int getVersion() { - return 0; - } - - @Override - public void onCreate(Context sysuiContext, Context pluginContext) { - } - - @Override - public void onDestroy() { - } + protected void onNotificationsClick(View v) { + mCarStatusBar.togglePanel(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationButton.java index 2de358f1c292..0cdaec1432c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationButton.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarNavigationButton.java @@ -1,72 +1,87 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package com.android.systemui.statusbar.car; import android.content.Context; -import android.graphics.drawable.Drawable; +import android.content.Intent; +import android.content.res.TypedArray; import android.util.AttributeSet; import android.widget.ImageView; -import android.widget.RelativeLayout; -import com.android.keyguard.AlphaOptimizedImageButton; import com.android.systemui.R; +import java.net.URISyntaxException; + /** - * A wrapper view for a car navigation facet, which includes a button icon and a drop down icon. + * CarNavigationButton is an image button that allows for a bit more configuration at the + * xml file level. This allows for more control via overlays instead of having to update + * code. */ -public class CarNavigationButton extends RelativeLayout { +public class CarNavigationButton extends com.android.keyguard.AlphaOptimizedImageButton { + private static final float SELECTED_ALPHA = 1; private static final float UNSELECTED_ALPHA = 0.7f; - private AlphaOptimizedImageButton mIcon; - private AlphaOptimizedImageButton mMoreIcon; + private Context mContext; + private String mIntent = null; + private String mLongIntent = null; + private boolean mBroadcastIntent = false; + private boolean mSelected = false; + public CarNavigationButton(Context context, AttributeSet attrs) { super(context, attrs); + mContext = context; + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CarNavigationButton); + mIntent = typedArray.getString(R.styleable.CarNavigationButton_intent); + mLongIntent = typedArray.getString(R.styleable.CarNavigationButton_longIntent); + mBroadcastIntent = typedArray.getBoolean(R.styleable.CarNavigationButton_broadcast, false); } + + /** + * After the standard inflate this then adds the xml defined intents to click and long click + * actions if defined. + */ @Override public void onFinishInflate() { super.onFinishInflate(); - mIcon = findViewById(R.id.car_nav_button_icon); - mIcon.setScaleType(ImageView.ScaleType.CENTER); - mIcon.setClickable(false); - mIcon.setBackgroundColor(android.R.color.transparent); - mIcon.setAlpha(UNSELECTED_ALPHA); - - mMoreIcon = findViewById(R.id.car_nav_button_more_icon); - mMoreIcon.setClickable(false); - mMoreIcon.setBackgroundColor(android.R.color.transparent); - mMoreIcon.setVisibility(INVISIBLE); - mMoreIcon.setImageDrawable(getContext().getDrawable(R.drawable.car_ic_arrow)); - mMoreIcon.setAlpha(UNSELECTED_ALPHA); - } + setScaleType(ImageView.ScaleType.CENTER); + setAlpha(UNSELECTED_ALPHA); + try { + if (mIntent != null) { + final Intent intent = Intent.parseUri(mIntent, Intent.URI_INTENT_SCHEME); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); + setOnClickListener(v -> { + if (mBroadcastIntent) { + mContext.sendBroadcast(intent); + return; + } + mContext.startActivity(intent); + }); + } + } catch (URISyntaxException e) { + throw new RuntimeException("Failed to attach intent", e); + } - public void setResources(Drawable icon) { - mIcon.setImageDrawable(icon); + try { + if (mLongIntent != null) { + final Intent intent = Intent.parseUri(mLongIntent, Intent.URI_INTENT_SCHEME); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); + setOnLongClickListener(v -> { + mContext.startActivity(intent); + return true; + }); + } + } catch (URISyntaxException e) { + throw new RuntimeException("Failed to attach long press intent", e); + } } - public void setSelected(boolean selected, boolean showMoreIcon) { - if (selected) { - mMoreIcon.setVisibility(showMoreIcon ? VISIBLE : INVISIBLE); - mMoreIcon.setAlpha(SELECTED_ALPHA); - mIcon.setAlpha(SELECTED_ALPHA); - } else { - mMoreIcon.setVisibility(INVISIBLE); - mIcon.setAlpha(UNSELECTED_ALPHA); - } + /** + * @param selected true if should indicate if this is a selected state, false otherwise + */ + public void setSelected(boolean selected) { + super.setSelected(selected); + mSelected = selected; + setAlpha(mSelected ? SELECTED_ALPHA : UNSELECTED_ALPHA); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java index 3ebeb4d45c26..c15a01330534 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java @@ -18,17 +18,14 @@ package com.android.systemui.statusbar.car; import android.app.ActivityManager; import android.app.ActivityOptions; -import android.content.BroadcastReceiver; -import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; -import android.service.notification.StatusBarNotification; import android.util.Log; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; @@ -46,10 +43,7 @@ import com.android.systemui.classifier.FalsingManager; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.recents.Recents; import com.android.systemui.recents.misc.SysUiTaskStackChangeListener; -import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.statusbar.ExpandableNotificationRow; -import com.android.systemui.statusbar.NotificationData; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment; import com.android.systemui.statusbar.phone.NavigationBarView; @@ -69,7 +63,6 @@ public class CarStatusBar extends StatusBar implements private TaskStackListenerImpl mTaskStackListener; - private CarNavigationBarController mController; private FullscreenUserSwitcher mFullscreenUserSwitcher; private CarBatteryController mCarBatteryController; @@ -78,15 +71,23 @@ public class CarStatusBar extends StatusBar implements private ConnectedDeviceSignalController mConnectedDeviceSignalController; private ViewGroup mNavigationBarWindow; + private ViewGroup mLeftNavigationBarWindow; + private ViewGroup mRightNavigationBarWindow; private CarNavigationBarView mNavigationBarView; + private CarNavigationBarView mLeftNavigationBarView; + private CarNavigationBarView mRightNavigationBarView; private final Object mQueueLock = new Object(); + private boolean mShowLeft; + private boolean mShowRight; + private boolean mShowBottom; + private CarFacetButtonController mCarFacetButtonController; + @Override public void start() { super.start(); mTaskStackListener = new TaskStackListenerImpl(); ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); - registerPackageChangeReceivers(); mStackScroller.setScrollingEnabled(true); @@ -104,6 +105,16 @@ public class CarStatusBar extends StatusBar implements mNavigationBarView = null; } + if (mLeftNavigationBarWindow != null) { + mWindowManager.removeViewImmediate(mLeftNavigationBarWindow); + mLeftNavigationBarView = null; + } + + if (mRightNavigationBarWindow != null) { + mWindowManager.removeViewImmediate(mRightNavigationBarWindow); + mRightNavigationBarView = null; + } + super.destroy(); } @@ -153,10 +164,36 @@ public class CarStatusBar extends StatusBar implements @Override protected void createNavigationBar() { + mCarFacetButtonController = new CarFacetButtonController(mContext); if (mNavigationBarView != null) { return; } + mShowBottom = mContext.getResources().getBoolean(R.bool.config_enableBottomNavigationBar); + if (mShowBottom) { + buildBottomBar(); + } + + int widthForSides = mContext.getResources().getDimensionPixelSize( + R.dimen.navigation_bar_height_car_mode); + + + mShowLeft = mContext.getResources().getBoolean(R.bool.config_enableLeftNavigationBar); + + if (mShowLeft) { + buildLeft(widthForSides); + } + + mShowRight = mContext.getResources().getBoolean(R.bool.config_enableRightNavigationBar); + + if (mShowRight) { + buildRight(widthForSides); + } + + } + + + private void buildBottomBar() { // SystemUI requires that the navigation bar view have a parent. Since the regular // StatusBar inflates navigation_bar_window as this parent view, use the same view for the // CarNavigationBarView. @@ -171,17 +208,15 @@ public class CarStatusBar extends StatusBar implements mNavigationBarView = (CarNavigationBarView) mNavigationBarWindow.getChildAt(0); if (mNavigationBarView == null) { Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar"); + throw new RuntimeException("Unable to build botom nav bar due to missing layout"); } + mNavigationBarView.setStatusBar(this); - mController = new CarNavigationBarController(mContext, mNavigationBarView, - this /* ActivityStarter*/); - mNavigationBarView.getBarTransitions().setAlwaysOpaque(true); WindowManager.LayoutParams lp = new WindowManager.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.TYPE_NAVIGATION_BAR, - WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING - | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, @@ -189,9 +224,74 @@ public class CarStatusBar extends StatusBar implements lp.setTitle("CarNavigationBar"); lp.windowAnimations = 0; + + mCarFacetButtonController.addCarNavigationBar(mNavigationBarView); mWindowManager.addView(mNavigationBarWindow, lp); } + private void buildLeft(int widthForSides) { + mLeftNavigationBarWindow = (ViewGroup) View.inflate(mContext, + R.layout.navigation_bar_window, null); + if (mLeftNavigationBarWindow == null) { + Log.e(TAG, "CarStatusBar failed inflate for R.layout.navigation_bar_window"); + } + + View.inflate(mContext, R.layout.car_left_navigation_bar, mLeftNavigationBarWindow); + mLeftNavigationBarView = (CarNavigationBarView) mLeftNavigationBarWindow.getChildAt(0); + if (mLeftNavigationBarView == null) { + Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar"); + throw new RuntimeException("Unable to build left nav bar due to missing layout"); + } + mLeftNavigationBarView.setStatusBar(this); + mCarFacetButtonController.addCarNavigationBar(mLeftNavigationBarView); + + WindowManager.LayoutParams leftlp = new WindowManager.LayoutParams( + widthForSides, LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, + PixelFormat.TRANSLUCENT); + leftlp.setTitle("LeftCarNavigationBar"); + leftlp.windowAnimations = 0; + leftlp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_SCREEN_DECOR; + leftlp.gravity = Gravity.LEFT; + mWindowManager.addView(mLeftNavigationBarWindow, leftlp); + } + + + private void buildRight(int widthForSides) { + mRightNavigationBarWindow = (ViewGroup) View.inflate(mContext, + R.layout.navigation_bar_window, null); + if (mRightNavigationBarWindow == null) { + Log.e(TAG, "CarStatusBar failed inflate for R.layout.navigation_bar_window"); + } + + View.inflate(mContext, R.layout.car_right_navigation_bar, mRightNavigationBarWindow); + mRightNavigationBarView = (CarNavigationBarView) mRightNavigationBarWindow.getChildAt(0); + if (mRightNavigationBarView == null) { + Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar"); + throw new RuntimeException("Unable to build right nav bar due to missing layout"); + } + mRightNavigationBarView.setStatusBar(this); + mCarFacetButtonController.addCarNavigationBar(mRightNavigationBarView); + + WindowManager.LayoutParams rightlp = new WindowManager.LayoutParams( + widthForSides, LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, + PixelFormat.TRANSLUCENT); + rightlp.setTitle("RightCarNavigationBar"); + rightlp.windowAnimations = 0; + rightlp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_SCREEN_DECOR; + rightlp.gravity = Gravity.RIGHT; + mWindowManager.addView(mRightNavigationBarWindow, rightlp); + } + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { //When executing dump() funciton simultaneously, we need to serialize them @@ -204,8 +304,8 @@ public class CarStatusBar extends StatusBar implements } pw.print(" mTaskStackListener="); pw.println(mTaskStackListener); - pw.print(" mController="); - pw.println(mController); + pw.print(" mCarFacetButtonController="); + pw.println(mCarFacetButtonController); pw.print(" mFullscreenUserSwitcher="); pw.println(mFullscreenUserSwitcher); pw.print(" mCarBatteryController="); pw.println(mCarBatteryController); @@ -229,10 +329,6 @@ public class CarStatusBar extends StatusBar implements } } - @Override - public NavigationBarView getNavigationBarView() { - return mNavigationBarView; - } @Override public View getNavigationBarWindow() { @@ -269,24 +365,6 @@ public class CarStatusBar extends StatusBar implements } } - private BroadcastReceiver mPackageChangeReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getData() == null || mController == null) { - return; - } - String packageName = intent.getData().getSchemeSpecificPart(); - mController.onPackageChange(packageName); - } - }; - - private void registerPackageChangeReceivers() { - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_PACKAGE_ADDED); - filter.addAction(Intent.ACTION_PACKAGE_REMOVED); - filter.addDataScheme("package"); - mContext.registerReceiver(mPackageChangeReceiver, filter); - } public boolean hasDockedTask() { return Recents.getSystemServices().hasDockedTask(); @@ -301,10 +379,7 @@ public class CarStatusBar extends StatusBar implements public void onTaskStackChanged() { ActivityManager.RunningTaskInfo runningTaskInfo = ActivityManagerWrapper.getInstance().getRunningTask(); - if (runningTaskInfo != null && runningTaskInfo.baseActivity != null) { - mController.taskChanged(runningTaskInfo.baseActivity.getPackageName(), - runningTaskInfo); - } + mCarFacetButtonController.taskChanged(runningTaskInfo); } } @@ -346,40 +421,13 @@ public class CarStatusBar extends StatusBar implements // Do nothing, we don't want to display media art in the lock screen for a car. } - private int startActivityWithOptions(Intent intent, Bundle options) { - int result = ActivityManager.START_CANCELED; - try { - result = ActivityManager.getService().startActivityAsUser(null /* caller */, - mContext.getBasePackageName(), - intent, - intent.resolveTypeIfNeeded(mContext.getContentResolver()), - null /* resultTo*/, - null /* resultWho*/, - 0 /* requestCode*/, - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP, - null /* profilerInfo*/, - options, - UserHandle.CURRENT.getIdentifier()); - } catch (RemoteException e) { - Log.w(TAG, "Unable to start activity", e); - } - - return result; - } - - public int startActivityOnStack(Intent intent, int windowingMode, int activityType) { - final ActivityOptions options = ActivityOptions.makeBasic(); - options.setLaunchWindowingMode(windowingMode); - options.setLaunchActivityType(activityType); - return startActivityWithOptions(intent, options.toBundle()); - } @Override public void animateExpandNotificationsPanel() { // Because space is usually constrained in the auto use-case, there should not be a // pinned notification when the shade has been expanded. Ensure this by removing all heads- // up notifications. - mHeadsUpManager.removeAllHeadsUpEntries(); + mHeadsUpManager.releaseAllImmediately(); super.animateExpandNotificationsPanel(); } @@ -390,8 +438,6 @@ public class CarStatusBar extends StatusBar implements @Override public void onDensityOrFontScaleChanged() { super.onDensityOrFontScaleChanged(); - mController.onDensityOrFontScaleChanged(); - // Need to update the background on density changed in case the change was due to night // mode. mNotificationPanelBackground = getDefaultWallpaper(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java index 677fa81a12cd..0304086dbfda 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/car/ConnectedDeviceSignalController.java @@ -16,10 +16,10 @@ import android.util.Log; import android.util.TypedValue; import android.view.View; import android.widget.ImageView; +import com.android.settingslib.graph.SignalDrawable; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.statusbar.ScalingDrawableWrapper; -import com.android.systemui.statusbar.phone.SignalDrawable; import com.android.systemui.statusbar.policy.BluetoothController; import static com.android.systemui.statusbar.phone.StatusBar.DEBUG; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java index 11d20b221051..ef44ad17e1c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java @@ -77,11 +77,11 @@ public class ActivityLaunchAnimator { mStatusBar = statusBar; } - public ActivityOptions getLaunchAnimation( - ExpandableNotificationRow sourceNofitication) { - AnimationRunner animationRunner = new AnimationRunner(sourceNofitication); - return ActivityOptions.makeRemoteAnimation( - new RemoteAnimationAdapter(animationRunner, 1000 /* Duration */, 0 /* delay */)); + public RemoteAnimationAdapter getLaunchAnimation( + ExpandableNotificationRow sourceNotification) { + AnimationRunner animationRunner = new AnimationRunner(sourceNotification); + return new RemoteAnimationAdapter(animationRunner, ANIMATION_DURATION, + 0 /* statusBarTransitionDelay */); } public boolean isAnimationPending() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ImageTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ImageTransformState.java index 8227b7797706..d3a325d3b91a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ImageTransformState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ImageTransformState.java @@ -21,6 +21,8 @@ import android.util.Pools; import android.view.View; import android.widget.ImageView; +import com.android.internal.widget.MessagingImageMessage; +import com.android.internal.widget.MessagingMessage; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.statusbar.CrossFadeHelper; @@ -117,13 +119,15 @@ public class ImageTransformState extends TransformState { @Override protected boolean transformScale(TransformState otherState) { - return true; + return sameAs(otherState); } @Override public void recycle() { super.recycle(); - sInstancePool.release(this); + if (getClass() == ImageTransformState.class) { + sInstancePool.release(this); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingImageTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingImageTransformState.java new file mode 100644 index 000000000000..b97995dd5f93 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingImageTransformState.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 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.systemui.statusbar.notification; + +import android.util.Pools; +import android.view.View; + +import com.android.internal.widget.MessagingImageMessage; +import com.android.systemui.Interpolators; +import com.android.systemui.R; +import com.android.systemui.statusbar.ViewTransformationHelper; + +/** + * A transform state of a image view. +*/ +public class MessagingImageTransformState extends ImageTransformState { + private static Pools.SimplePool<MessagingImageTransformState> sInstancePool + = new Pools.SimplePool<>(40); + private static final int START_ACTUAL_WIDTH = R.id.transformation_start_actual_width; + private static final int START_ACTUAL_HEIGHT = R.id.transformation_start_actual_height; + private MessagingImageMessage mImageMessage; + + @Override + public void initFrom(View view, TransformInfo transformInfo) { + super.initFrom(view, transformInfo); + mImageMessage = (MessagingImageMessage) view; + } + + @Override + protected boolean sameAs(TransformState otherState) { + if (super.sameAs(otherState)) { + return true; + } + if (otherState instanceof MessagingImageTransformState) { + MessagingImageTransformState otherMessage = (MessagingImageTransformState) otherState; + return mImageMessage.sameAs(otherMessage.mImageMessage); + } + return false; + } + + public static MessagingImageTransformState obtain() { + MessagingImageTransformState instance = sInstancePool.acquire(); + if (instance != null) { + return instance; + } + return new MessagingImageTransformState(); + } + + @Override + protected boolean transformScale(TransformState otherState) { + return false; + } + + @Override + protected void transformViewFrom(TransformState otherState, int transformationFlags, + ViewTransformationHelper.CustomTransformation customTransformation, + float transformationAmount) { + super.transformViewFrom(otherState, transformationFlags, customTransformation, + transformationAmount); + float interpolatedValue = mDefaultInterpolator.getInterpolation( + transformationAmount); + if (otherState instanceof MessagingImageTransformState && sameAs(otherState)) { + MessagingImageMessage otherMessage + = ((MessagingImageTransformState) otherState).mImageMessage; + if (transformationAmount == 0.0f) { + setStartActualWidth(otherMessage.getActualWidth()); + setStartActualHeight(otherMessage.getActualHeight()); + } + float startActualWidth = getStartActualWidth(); + mImageMessage.setActualWidth( + (int) NotificationUtils.interpolate(startActualWidth, + mImageMessage.getStaticWidth(), + interpolatedValue)); + float startActualHeight = getStartActualHeight(); + mImageMessage.setActualHeight( + (int) NotificationUtils.interpolate(startActualHeight, + mImageMessage.getHeight(), + interpolatedValue)); + } + } + + public int getStartActualWidth() { + Object tag = mTransformedView.getTag(START_ACTUAL_WIDTH); + return tag == null ? -1 : (int) tag; + } + + public void setStartActualWidth(int actualWidth) { + mTransformedView.setTag(START_ACTUAL_WIDTH, actualWidth); + } + + public int getStartActualHeight() { + Object tag = mTransformedView.getTag(START_ACTUAL_HEIGHT); + return tag == null ? -1 : (int) tag; + } + + public void setStartActualHeight(int actualWidth) { + mTransformedView.setTag(START_ACTUAL_HEIGHT, actualWidth); + } + + @Override + public void recycle() { + super.recycle(); + if (getClass() == MessagingImageTransformState.class) { + sInstancePool.release(this); + } + } + + @Override + protected void resetTransformedView() { + super.resetTransformedView(); + mImageMessage.setActualWidth(mImageMessage.getStaticWidth()); + mImageMessage.setActualHeight(mImageMessage.getHeight()); + } + + @Override + protected void reset() { + super.reset(); + mImageMessage = null; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java index 113118a1c8c5..314a31d336fd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java @@ -22,6 +22,7 @@ import android.view.View; import android.view.ViewGroup; import com.android.internal.widget.MessagingGroup; +import com.android.internal.widget.MessagingImageMessage; import com.android.internal.widget.MessagingLayout; import com.android.internal.widget.MessagingLinearLayout; import com.android.internal.widget.MessagingMessage; @@ -30,6 +31,7 @@ import com.android.systemui.Interpolators; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; /** * A transform state of the action list @@ -156,6 +158,7 @@ public class MessagingLayoutTransformState extends TransformState { } appear(ownGroup.getAvatar(), transformationAmount); appear(ownGroup.getSenderView(), transformationAmount); + appear(ownGroup.getIsolatedMessage(), transformationAmount); setClippingDeactivated(ownGroup.getSenderView(), true); setClippingDeactivated(ownGroup.getAvatar(), true); } @@ -187,12 +190,13 @@ public class MessagingLayoutTransformState extends TransformState { } disappear(ownGroup.getAvatar(), transformationAmount); disappear(ownGroup.getSenderView(), transformationAmount); + disappear(ownGroup.getIsolatedMessage(), transformationAmount); setClippingDeactivated(ownGroup.getSenderView(), true); setClippingDeactivated(ownGroup.getAvatar(), true); } private void appear(View child, float transformationAmount) { - if (child.getVisibility() == View.GONE) { + if (child == null || child.getVisibility() == View.GONE) { return; } TransformState ownState = TransformState.createFrom(child, mTransformInfo); @@ -201,7 +205,7 @@ public class MessagingLayoutTransformState extends TransformState { } private void disappear(View child, float transformationAmount) { - if (child.getVisibility() == View.GONE) { + if (child == null || child.getVisibility() == View.GONE) { return; } TransformState ownState = TransformState.createFrom(child, mTransformInfo); @@ -224,22 +228,24 @@ public class MessagingLayoutTransformState extends TransformState { private void transformGroups(MessagingGroup ownGroup, MessagingGroup otherGroup, float transformationAmount, boolean to) { + boolean useLinearTransformation = + otherGroup.getIsolatedMessage() == null && !mTransformInfo.isAnimating(); transformView(transformationAmount, to, ownGroup.getSenderView(), otherGroup.getSenderView(), - true /* sameAsAny */); + true /* sameAsAny */, useLinearTransformation); transformView(transformationAmount, to, ownGroup.getAvatar(), otherGroup.getAvatar(), - true /* sameAsAny */); - MessagingLinearLayout ownMessages = ownGroup.getMessageContainer(); - MessagingLinearLayout otherMessages = otherGroup.getMessageContainer(); + true /* sameAsAny */, useLinearTransformation); + List<MessagingMessage> ownMessages = ownGroup.getMessages(); + List<MessagingMessage> otherMessages = otherGroup.getMessages(); float previousTranslation = 0; - for (int i = 0; i < ownMessages.getChildCount(); i++) { - View child = ownMessages.getChildAt(ownMessages.getChildCount() - 1 - i); + for (int i = 0; i < ownMessages.size(); i++) { + View child = ownMessages.get(ownMessages.size() - 1 - i).getView(); if (isGone(child)) { continue; } - int otherIndex = otherMessages.getChildCount() - 1 - i; + int otherIndex = otherMessages.size() - 1 - i; View otherChild = null; if (otherIndex >= 0) { - otherChild = otherMessages.getChildAt(otherIndex); + otherChild = otherMessages.get(otherIndex).getView(); if (isGone(otherChild)) { otherChild = null; } @@ -252,7 +258,12 @@ public class MessagingLayoutTransformState extends TransformState { transformationAmount = 1.0f - transformationAmount; } } - transformView(transformationAmount, to, child, otherChild, false /* sameAsAny */); + transformView(transformationAmount, to, child, otherChild, false, /* sameAsAny */ + useLinearTransformation); + if (transformationAmount == 0.0f + && otherGroup.getIsolatedMessage() == otherChild) { + ownGroup.setTransformingImages(true); + } if (otherChild == null) { child.setTranslationY(previousTranslation); setClippingDeactivated(child, true); @@ -264,12 +275,13 @@ public class MessagingLayoutTransformState extends TransformState { previousTranslation = child.getTranslationY(); } } + ownGroup.updateClipRect(); } private void transformView(float transformationAmount, boolean to, View ownView, - View otherView, boolean sameAsAny) { + View otherView, boolean sameAsAny, boolean useLinearTransformation) { TransformState ownState = TransformState.createFrom(ownView, mTransformInfo); - if (!mTransformInfo.isAnimating()) { + if (useLinearTransformation) { ownState.setDefaultInterpolator(Interpolators.LINEAR); } ownState.setIsSameAsAnyView(sameAsAny); @@ -339,11 +351,15 @@ public class MessagingLayoutTransformState extends TransformState { if (!isGone(ownGroup)) { MessagingLinearLayout ownMessages = ownGroup.getMessageContainer(); for (int j = 0; j < ownMessages.getChildCount(); j++) { - MessagingMessage child = (MessagingMessage) ownMessages.getChildAt(j); + View child = ownMessages.getChildAt(j); setVisible(child, visible, force); } setVisible(ownGroup.getAvatar(), visible, force); setVisible(ownGroup.getSenderView(), visible, force); + MessagingImageMessage isolatedMessage = ownGroup.getIsolatedMessage(); + if (isolatedMessage != null) { + setVisible(isolatedMessage, visible, force); + } } } } @@ -375,11 +391,17 @@ public class MessagingLayoutTransformState extends TransformState { } resetTransformedView(ownGroup.getAvatar()); resetTransformedView(ownGroup.getSenderView()); + MessagingImageMessage isolatedMessage = ownGroup.getIsolatedMessage(); + if (isolatedMessage != null) { + resetTransformedView(isolatedMessage); + } setClippingDeactivated(ownGroup.getAvatar(), false); setClippingDeactivated(ownGroup.getSenderView(), false); ownGroup.setTranslationY(0); ownGroup.getMessageContainer().setTranslationY(0); } + ownGroup.setTransformingImages(false); + ownGroup.updateClipRect(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java index 918b6edc0c30..fc8ceb6620a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java @@ -26,6 +26,7 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; +import com.android.internal.widget.MessagingImageMessage; import com.android.internal.widget.MessagingPropertyAnimator; import com.android.internal.widget.ViewClippingUtil; import com.android.systemui.Interpolators; @@ -80,7 +81,7 @@ public class TransformState { private boolean mSameAsAny; private float mTransformationEndY = UNDEFINED; private float mTransformationEndX = UNDEFINED; - private Interpolator mDefaultInterpolator = Interpolators.FAST_OUT_SLOW_IN; + protected Interpolator mDefaultInterpolator = Interpolators.FAST_OUT_SLOW_IN; public void initFrom(View view, TransformInfo transformInfo) { mTransformedView = view; @@ -131,7 +132,7 @@ public class TransformState { transformViewFrom(otherState, TRANSFORM_Y, null, transformationAmount); } - private void transformViewFrom(TransformState otherState, int transformationFlags, + protected void transformViewFrom(TransformState otherState, int transformationFlags, ViewTransformationHelper.CustomTransformation customTransformation, float transformationAmount) { final View transformedView = mTransformedView; @@ -449,6 +450,11 @@ public class TransformState { result.initFrom(view, transformInfo); return result; } + if (view instanceof MessagingImageMessage) { + MessagingImageTransformState result = MessagingImageTransformState.obtain(); + result.initFrom(view, transformInfo); + return result; + } if (view instanceof ImageView) { ImageTransformState result = ImageTransformState.obtain(); result.initFrom(view, transformInfo); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java new file mode 100644 index 000000000000..aba5cdf0ca2b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -0,0 +1,446 @@ +/* + * Copyright (C) 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.systemui.statusbar.phone; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.support.v4.util.ArraySet; +import android.util.Log; +import android.util.Pools; +import android.view.View; +import android.view.ViewTreeObserver; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.Dumpable; +import com.android.systemui.statusbar.ExpandableNotificationRow; +import com.android.systemui.statusbar.NotificationData; +import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.notification.VisualStabilityManager; +import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.Stack; + +/** + * A implementation of HeadsUpManager for phone and car. + */ +public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, + ViewTreeObserver.OnComputeInternalInsetsListener, VisualStabilityManager.Callback, + OnHeadsUpChangedListener { + private static final String TAG = "HeadsUpManagerPhone"; + private static final boolean DEBUG = false; + + private final View mStatusBarWindowView; + private final int mStatusBarHeight; + private final NotificationGroupManager mGroupManager; + private final StatusBar mBar; + private final VisualStabilityManager mVisualStabilityManager; + + private boolean mReleaseOnExpandFinish; + private boolean mTrackingHeadsUp; + private HashSet<String> mSwipedOutKeys = new HashSet<>(); + private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>(); + private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed + = new ArraySet<>(); + private boolean mIsExpanded; + private int[] mTmpTwoArray = new int[2]; + private boolean mHeadsUpGoingAway; + private boolean mWaitingOnCollapseWhenGoingAway; + private boolean mIsObserving; + private int mStatusBarState; + + private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() { + private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>(); + + @Override + public HeadsUpEntryPhone acquire() { + if (!mPoolObjects.isEmpty()) { + return mPoolObjects.pop(); + } + return new HeadsUpEntryPhone(); + } + + @Override + public boolean release(@NonNull HeadsUpEntryPhone instance) { + mPoolObjects.push(instance); + return true; + } + }; + + /////////////////////////////////////////////////////////////////////////////////////////////// + // Constructor: + + public HeadsUpManagerPhone(@NonNull final Context context, @NonNull View statusBarWindowView, + @NonNull NotificationGroupManager groupManager, @NonNull StatusBar bar, + @NonNull VisualStabilityManager visualStabilityManager) { + super(context); + + mStatusBarWindowView = statusBarWindowView; + mGroupManager = groupManager; + mBar = bar; + mVisualStabilityManager = visualStabilityManager; + + Resources resources = mContext.getResources(); + mStatusBarHeight = resources.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + + addListener(new OnHeadsUpChangedListener() { + @Override + public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) { + if (DEBUG) Log.w(TAG, "onHeadsUpPinnedModeChanged"); + updateTouchableRegionListener(); + } + }); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // Public methods: + + /** + * Decides whether a click is invalid for a notification, i.e it has not been shown long enough + * that a user might have consciously clicked on it. + * + * @param key the key of the touched notification + * @return whether the touch is invalid and should be discarded + */ + public boolean shouldSwallowClick(@NonNull String key) { + HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key); + return entry != null && mClock.currentTimeMillis() < entry.postTime; + } + + public void onExpandingFinished() { + if (mReleaseOnExpandFinish) { + releaseAllImmediately(); + mReleaseOnExpandFinish = false; + } else { + for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) { + if (isHeadsUp(entry.key)) { + // Maybe the heads-up was removed already + removeHeadsUpEntry(entry); + } + } + } + mEntriesToRemoveAfterExpand.clear(); + } + + /** + * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry + * from the list even after a Heads Up Notification is gone. + */ + public void setTrackingHeadsUp(boolean trackingHeadsUp) { + mTrackingHeadsUp = trackingHeadsUp; + } + + /** + * Notify that the status bar panel gets expanded or collapsed. + * + * @param isExpanded True to notify expanded, false to notify collapsed. + */ + public void setIsPanelExpanded(boolean isExpanded) { + if (isExpanded != mIsExpanded) { + mIsExpanded = isExpanded; + if (isExpanded) { + // make sure our state is sane + mWaitingOnCollapseWhenGoingAway = false; + mHeadsUpGoingAway = false; + updateTouchableRegionListener(); + } + } + } + + /** + * Set the current state of the statusbar. + */ + public void setStatusBarState(int statusBarState) { + mStatusBarState = statusBarState; + } + + /** + * Set that we are exiting the headsUp pinned mode, but some notifications might still be + * animating out. This is used to keep the touchable regions in a sane state. + */ + public void setHeadsUpGoingAway(boolean headsUpGoingAway) { + if (headsUpGoingAway != mHeadsUpGoingAway) { + mHeadsUpGoingAway = headsUpGoingAway; + if (!headsUpGoingAway) { + waitForStatusBarLayout(); + } + updateTouchableRegionListener(); + } + } + + /** + * Notifies that a remote input textbox in notification gets active or inactive. + * @param entry The entry of the target notification. + * @param remoteInputActive True to notify active, False to notify inactive. + */ + public void setRemoteInputActive( + @NonNull NotificationData.Entry entry, boolean remoteInputActive) { + HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.key); + if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) { + headsUpEntry.remoteInputActive = remoteInputActive; + if (remoteInputActive) { + headsUpEntry.removeAutoRemovalCallbacks(); + } else { + headsUpEntry.updateEntry(false /* updatePostTime */); + } + } + } + + @VisibleForTesting + public void removeMinimumDisplayTimeForTesting() { + mMinimumDisplayTime = 0; + mHeadsUpNotificationDecay = 0; + mTouchAcceptanceDelay = 0; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HeadsUpManager public methods overrides: + + @Override + public boolean isTrackingHeadsUp() { + return mTrackingHeadsUp; + } + + @Override + public void snooze() { + super.snooze(); + mReleaseOnExpandFinish = true; + } + + /** + * 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 ignoreEarliestRemovalTime) { + if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) { + return super.removeNotification(key, ignoreEarliestRemovalTime); + } else { + HeadsUpEntryPhone entry = getHeadsUpEntryPhone(key); + entry.removeAsSoonAsPossible(); + return false; + } + } + + public void addSwipedOutNotification(@NonNull String key) { + mSwipedOutKeys.add(key); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // Dumpable overrides: + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("HeadsUpManagerPhone state:"); + dumpInternal(fd, pw, args); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // ViewTreeObserver.OnComputeInternalInsetsListener overrides: + + /** + * Overridden from TreeObserver. + */ + @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { + if (mIsExpanded || mBar.isBouncerShowing()) { + // The touchable region is always the full area when expanded + return; + } + if (hasPinnedHeadsUp()) { + ExpandableNotificationRow topEntry = getTopEntry().row; + if (topEntry.isChildInGroup()) { + final ExpandableNotificationRow groupSummary + = mGroupManager.getGroupSummary(topEntry.getStatusBarNotification()); + if (groupSummary != null) { + topEntry = groupSummary; + } + } + topEntry.getLocationOnScreen(mTmpTwoArray); + int minX = mTmpTwoArray[0]; + int maxX = mTmpTwoArray[0] + topEntry.getWidth(); + int maxY = topEntry.getIntrinsicHeight(); + + info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + info.touchableRegion.set(minX, 0, maxX, maxY); + } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) { + info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // VisualStabilityManager.Callback overrides: + + @Override + public void onReorderingAllowed() { + mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false); + for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) { + if (isHeadsUp(entry.key)) { + // Maybe the heads-up was removed already + removeHeadsUpEntry(entry); + } + } + mEntriesToRemoveWhenReorderingAllowed.clear(); + mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(true); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HeadsUpManager utility (protected) methods overrides: + + @Override + protected HeadsUpEntry createHeadsUpEntry() { + return mEntryPool.acquire(); + } + + @Override + protected void releaseHeadsUpEntry(HeadsUpEntry entry) { + entry.reset(); + mEntryPool.release((HeadsUpEntryPhone) entry); + } + + @Override + protected boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) { + return mStatusBarState != StatusBarState.KEYGUARD && !mIsExpanded + || super.shouldHeadsUpBecomePinned(entry); + } + + @Override + protected void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) { + super.dumpInternal(fd, pw, args); + pw.print(" mStatusBarState="); pw.println(mStatusBarState); + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // Private utility methods: + + @Nullable + private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) { + return (HeadsUpEntryPhone) getHeadsUpEntry(key); + } + + @Nullable + private HeadsUpEntryPhone getTopHeadsUpEntryPhone() { + return (HeadsUpEntryPhone) getTopHeadsUpEntry(); + } + + private boolean wasShownLongEnough(@NonNull String key) { + if (mSwipedOutKeys.contains(key)) { + // We always instantly dismiss views being manually swiped out. + mSwipedOutKeys.remove(key); + return true; + } + + HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key); + HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone(); + return headsUpEntry != topEntry || headsUpEntry.wasShownLongEnough(); + } + + /** + * We need to wait on the whole panel to collapse, before we can remove the touchable region + * listener. + */ + private void waitForStatusBarLayout() { + mWaitingOnCollapseWhenGoingAway = true; + mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, + int oldTop, int oldRight, int oldBottom) { + if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) { + mStatusBarWindowView.removeOnLayoutChangeListener(this); + mWaitingOnCollapseWhenGoingAway = false; + updateTouchableRegionListener(); + } + } + }); + } + + private void updateTouchableRegionListener() { + boolean shouldObserve = hasPinnedHeadsUp() || mHeadsUpGoingAway + || mWaitingOnCollapseWhenGoingAway; + if (shouldObserve == mIsObserving) { + return; + } + if (shouldObserve) { + mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this); + mStatusBarWindowView.requestLayout(); + } else { + mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + } + mIsObserving = shouldObserve; + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // HeadsUpEntryPhone: + + protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry { + public void setEntry(@NonNull final NotificationData.Entry entry) { + Runnable removeHeadsUpRunnable = () -> { + if (!mVisualStabilityManager.isReorderingAllowed()) { + mEntriesToRemoveWhenReorderingAllowed.add(entry); + mVisualStabilityManager.addReorderingAllowedCallback( + HeadsUpManagerPhone.this); + } else if (!mTrackingHeadsUp) { + removeHeadsUpEntry(entry); + } else { + mEntriesToRemoveAfterExpand.add(entry); + } + }; + + super.setEntry(entry, removeHeadsUpRunnable); + } + + public boolean wasShownLongEnough() { + return earliestRemovaltime < mClock.currentTimeMillis(); + } + + @Override + public void updateEntry(boolean updatePostTime) { + super.updateEntry(updatePostTime); + + if (mEntriesToRemoveAfterExpand.contains(entry)) { + mEntriesToRemoveAfterExpand.remove(entry); + } + if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) { + mEntriesToRemoveWhenReorderingAllowed.remove(entry); + } + } + + @Override + public void expanded(boolean expanded) { + if (this.expanded == expanded) { + return; + } + + this.expanded = expanded; + if (expanded) { + removeAutoRemovalCallbacks(); + } else { + updateEntry(false /* updatePostTime */); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java index c85571c1895d..2bfdefe39017 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpTouchHelper.java @@ -23,7 +23,7 @@ import android.view.ViewConfiguration; import com.android.systemui.Gefingerpoken; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; -import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.phone.HeadsUpManagerPhone; import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; /** @@ -31,7 +31,7 @@ import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; */ public class HeadsUpTouchHelper implements Gefingerpoken { - private HeadsUpManager mHeadsUpManager; + private HeadsUpManagerPhone mHeadsUpManager; private NotificationStackScrollLayout mStackScroller; private int mTrackingPointer; private float mTouchSlop; @@ -43,7 +43,7 @@ public class HeadsUpTouchHelper implements Gefingerpoken { private NotificationPanelView mPanel; private ExpandableNotificationRow mPickedChild; - public HeadsUpTouchHelper(HeadsUpManager headsUpManager, + public HeadsUpTouchHelper(HeadsUpManagerPhone headsUpManager, NotificationStackScrollLayout stackScroller, NotificationPanelView notificationPanelView) { mHeadsUpManager = headsUpManager; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java index 1239a9ea0240..79c605e4ed23 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java @@ -19,6 +19,7 @@ import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; import static android.app.StatusBarManager.windowStateToString; +import static com.android.systemui.shared.system.NavigationBarCompat.InteractionType; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_SEMI_TRANSPARENT; import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_WINDOW_STATE; import static com.android.systemui.statusbar.phone.StatusBar.dumpBarTransitions; @@ -71,7 +72,6 @@ import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener; -import android.widget.Button; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -114,8 +114,6 @@ public class NavigationBarFragment extends Fragment implements Callbacks { /** Allow some time inbetween the long press for back and recents. */ private static final int LOCK_TO_APP_GESTURE_TOLERENCE = 200; - private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100; - protected NavigationBarView mNavigationBarView = null; protected AssistManager mAssistManager; @@ -152,7 +150,7 @@ public class NavigationBarFragment extends Fragment implements Callbacks { private RotationLockController mRotationLockController; private TaskStackListenerImpl mTaskStackListener; - private final Runnable mRemoveRotationProposal = () -> setRotateSuggestionButtonState(false); + private final Runnable mRemoveRotationProposal = () -> safeSetRotationButtonState(false); private Animator mRotateShowAnimator; private Animator mRotateHideAnimator; @@ -167,6 +165,11 @@ public class NavigationBarFragment extends Fragment implements Callbacks { public void onRecentsAnimationStarted() { mNavigationBarView.setRecentsAnimationStarted(true); } + + @Override + public void onInteractionFlagsChanged(@InteractionType int flags) { + mNavigationBarView.updateStates(); + } }; // ----- Fragment Lifecycle Callbacks ----- @@ -361,22 +364,32 @@ public class NavigationBarFragment extends Fragment implements Callbacks { // rotate button if shown. if (!isValid) { - setRotateSuggestionButtonState(false); + safeSetRotationButtonState(false); return; } if (rotation == mWindowManager.getDefaultDisplay().getRotation()) { // Use this as a signal to remove any current suggestions getView().getHandler().removeCallbacks(mRemoveRotationProposal); - setRotateSuggestionButtonState(false); + safeSetRotationButtonState(false); } else { mLastRotationSuggestion = rotation; // Remember rotation for click - setRotateSuggestionButtonState(true); + safeSetRotationButtonState(true); rescheduleRotationTimeout(false); mMetricsLogger.visible(MetricsEvent.ROTATION_SUGGESTION_SHOWN); } } + private void safeSetRotationButtonState(boolean vis) { + if (mNavigationBarView != null) mNavigationBarView.setRotateSuggestionButtonState(vis); + } + + private void safeSetRotationButtonState(boolean vis, boolean force) { + if (mNavigationBarView != null) { + mNavigationBarView.setRotateSuggestionButtonState(vis, force); + } + } + private void rescheduleRotationTimeout(final boolean reasonHover) { // May be called due to a new rotation proposal or a change in hover state if (reasonHover) { @@ -402,84 +415,6 @@ public class NavigationBarFragment extends Fragment implements Callbacks { return 6000; } - public void setRotateSuggestionButtonState(final boolean visible) { - setRotateSuggestionButtonState(visible, false); - } - - public void setRotateSuggestionButtonState(final boolean visible, final boolean skipAnim) { - ButtonDispatcher rotBtn = mNavigationBarView.getRotateSuggestionButton(); - final boolean currentlyVisible = rotBtn.getVisibility() == View.VISIBLE; - - // Rerun a show animation to indicate change but don't rerun a hide animation - if (!visible && !currentlyVisible) return; - - View currentView = rotBtn.getCurrentView(); - if (currentView == null) return; - - KeyButtonDrawable kbd = rotBtn.getImageDrawable(); - if (kbd == null) return; - - AnimatedVectorDrawable animIcon = null; - if (kbd.getDrawable(0) instanceof AnimatedVectorDrawable) { - animIcon = (AnimatedVectorDrawable) kbd.getDrawable(0); - } - - if (visible) { // Appear and change - rotBtn.setVisibility(View.VISIBLE); - mNavigationBarView.notifySubtreeAccessibilityStateChangedIfNeeded(); - - if (skipAnim) { - currentView.setAlpha(1f); - return; - } - - // Start a new animation if running - if (mRotateShowAnimator != null) mRotateShowAnimator.pause(); - if (mRotateHideAnimator != null) mRotateHideAnimator.pause(); - - ObjectAnimator appearFade = ObjectAnimator.ofFloat(currentView, "alpha", - 0f, 1f); - appearFade.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS); - appearFade.setInterpolator(Interpolators.LINEAR); - mRotateShowAnimator = appearFade; - appearFade.start(); - - // Run the rotate icon's animation if it has one - if (animIcon != null) { - animIcon.reset(); - animIcon.start(); - } - - } else { // Hide - - if (skipAnim) { - rotBtn.setVisibility(View.INVISIBLE); - mNavigationBarView.notifySubtreeAccessibilityStateChangedIfNeeded(); - return; - } - - // Don't start any new hide animations if one is running - if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return; - // Pause any active show animations but don't reset the AVD to avoid jumps - if (mRotateShowAnimator != null) mRotateShowAnimator.pause(); - - ObjectAnimator fadeOut = ObjectAnimator.ofFloat(currentView, "alpha", - 0f); - fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS); - fadeOut.setInterpolator(Interpolators.LINEAR); - fadeOut.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - rotBtn.setVisibility(View.INVISIBLE); - mNavigationBarView.notifySubtreeAccessibilityStateChangedIfNeeded(); - } - }); - - mRotateHideAnimator = fadeOut; - fadeOut.start(); - } - } - // Injected from StatusBar at creation. public void setCurrentSysuiVisibility(int systemUiVisibility) { mSystemUiVisibility = systemUiVisibility; @@ -892,7 +827,7 @@ public class NavigationBarFragment extends Fragment implements Callbacks { if (shouldOverrideUserLockPrefs(rotation)) { mRotationLockController.setRotationLockedAtAngle(true, rotation); } - setRotateSuggestionButtonState(false, true); + safeSetRotationButtonState(false, true); } if (mNavigationBarView != null @@ -928,22 +863,22 @@ public class NavigationBarFragment extends Fragment implements Callbacks { @Override public void onTaskStackChanged() { - setRotateSuggestionButtonState(false); + safeSetRotationButtonState(false); } @Override public void onTaskRemoved(int taskId) { - setRotateSuggestionButtonState(false); + safeSetRotationButtonState(false); } @Override public void onTaskMovedToFront(int taskId) { - setRotateSuggestionButtonState(false); + safeSetRotationButtonState(false); } @Override public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) { - setRotateSuggestionButtonState(false); + safeSetRotationButtonState(false); } } @@ -960,6 +895,7 @@ public class NavigationBarFragment extends Fragment implements Callbacks { PixelFormat.TRANSLUCENT); lp.token = new Binder(); lp.setTitle("NavigationBar"); + lp.accessibilityTitle = context.getString(R.string.nav_bar); lp.windowAnimations = 0; View navigationBarView = LayoutInflater.from(context).inflate( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java index 0d36efda9ece..4454ef9f411c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java @@ -16,6 +16,12 @@ package com.android.systemui.statusbar.phone; +import static android.view.WindowManager.DOCKED_INVALID; +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_TOP; +import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY; +import static com.android.systemui.OverviewProxyService.TAG_OPS; + import android.app.ActivityManager; import android.content.Context; import android.content.res.Resources; @@ -27,26 +33,19 @@ import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; - import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget; import com.android.systemui.Dependency; import com.android.systemui.OverviewProxyService; +import com.android.systemui.OverviewProxyService.OverviewProxyListener; import com.android.systemui.R; import com.android.systemui.RecentsComponent; import com.android.systemui.SysUiServiceProvider; import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper; import com.android.systemui.shared.recents.IOverviewProxy; -import com.android.systemui.shared.recents.utilities.Utilities; import com.android.systemui.stackdivider.Divider; import com.android.systemui.tuner.TunerService; -import static android.view.WindowManager.DOCKED_INVALID; -import static android.view.WindowManager.DOCKED_LEFT; -import static android.view.WindowManager.DOCKED_TOP; -import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY; -import static com.android.systemui.OverviewProxyService.TAG_OPS; - /** * Class to detect gestures on the navigation bar. */ @@ -84,8 +83,16 @@ public class NavigationBarGestureHelper implements TunerService.Tunable, Gesture private int mTouchDownY; private boolean mDownOnRecents; private VelocityTracker mVelocityTracker; - private OverviewProxyService mOverviewEventSender = Dependency.get(OverviewProxyService.class); + private OverviewProxyService mOverviewProxyService = Dependency.get(OverviewProxyService.class); + private final OverviewProxyListener mOverviewProxyListener = new OverviewProxyListener() { + @Override + public void onRecentsAnimationStarted() { + mRecentsAnimationStarted = true; + mQuickScrubController.cancelQuickSwitch(); + } + }; + private boolean mRecentsAnimationStarted; private boolean mDockWindowEnabled; private boolean mDockWindowTouchSlopExceeded; private int mDragMode; @@ -97,10 +104,12 @@ public class NavigationBarGestureHelper implements TunerService.Tunable, Gesture mScrollTouchSlop = r.getDimensionPixelSize(R.dimen.navigation_bar_min_swipe_distance); mQuickScrubController = new QuickScrubController(context); Dependency.get(TunerService.class).addTunable(this, KEY_DOCK_WINDOW_GESTURE); + mOverviewProxyService.addCallback(mOverviewProxyListener); } public void destroy() { Dependency.get(TunerService.class).removeTunable(this); + mOverviewProxyService.removeCallback(mOverviewProxyListener); } public void setComponents(RecentsComponent recentsComponent, Divider divider, @@ -117,11 +126,14 @@ public class NavigationBarGestureHelper implements TunerService.Tunable, Gesture } private boolean proxyMotionEvents(MotionEvent event) { - final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy(); - if (overviewProxy != null) { + final IOverviewProxy overviewProxy = mOverviewProxyService.getProxy(); + if (overviewProxy != null && mNavigationBarView.isQuickStepSwipeUpEnabled()) { mNavigationBarView.requestUnbufferedDispatch(event); event.transform(mTransformGlobalMatrix); try { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + overviewProxy.onPreMotionEvent(mNavigationBarView.getDownHitTarget()); + } overviewProxy.onMotionEvent(event); if (DEBUG_OVERVIEW_PROXY) { Log.d(TAG_OPS, "Send MotionEvent: " + event.toString()); @@ -137,8 +149,12 @@ public class NavigationBarGestureHelper implements TunerService.Tunable, Gesture } public boolean onInterceptTouchEvent(MotionEvent event) { - int action = event.getAction(); - switch (action & MotionEvent.ACTION_MASK) { + if (mNavigationBarView.inScreenPinning()) { + return false; + } + + int action = event.getActionMasked(); + switch (action) { case MotionEvent.ACTION_DOWN: { mTouchDownX = (int) event.getX(); mTouchDownY = (int) event.getY(); @@ -146,19 +162,45 @@ public class NavigationBarGestureHelper implements TunerService.Tunable, Gesture mTransformLocalMatrix.set(Matrix.IDENTITY_MATRIX); mNavigationBarView.transformMatrixToGlobal(mTransformGlobalMatrix); mNavigationBarView.transformMatrixToLocal(mTransformLocalMatrix); + mRecentsAnimationStarted = false; break; } } - if (mStatusBar.isPresenterFullyCollapsed() - && !mQuickScrubController.onInterceptTouchEvent(event)) { + boolean handledByQuickscrub = mQuickScrubController.onInterceptTouchEvent(event); + if (mStatusBar.isPresenterFullyCollapsed() && !handledByQuickscrub) { + // Proxy motion events until we start intercepting for quickscrub proxyMotionEvents(event); + } + + boolean result = handledByQuickscrub; + result |= mRecentsAnimationStarted; + if (mDockWindowEnabled) { + result |= interceptDockWindowEvent(event); + } + return result; + } + + public boolean onTouchEvent(MotionEvent event) { + if (mNavigationBarView.inScreenPinning()) { return false; } - return (mDockWindowEnabled && interceptDockWindowEvent(event)); + + // The same down event was just sent on intercept and therefore can be ignored here + boolean ignoreProxyDownEvent = event.getAction() == MotionEvent.ACTION_DOWN + && mOverviewProxyService.getProxy() != null; + boolean result = mStatusBar.isPresenterFullyCollapsed() + && (mQuickScrubController.onTouchEvent(event) + || ignoreProxyDownEvent + || proxyMotionEvents(event)); + result |= mRecentsAnimationStarted; + if (mDockWindowEnabled) { + result |= handleDockWindowEvent(event); + } + return result; } public void onDraw(Canvas canvas) { - if (mOverviewEventSender.getProxy() != null) { + if (mNavigationBarView.isQuickScrubEnabled()) { mQuickScrubController.onDraw(canvas); } } @@ -307,20 +349,6 @@ public class NavigationBarGestureHelper implements TunerService.Tunable, Gesture return DRAG_MODE_RECENTS; } - public boolean onTouchEvent(MotionEvent event) { - // The same down event was just sent on intercept and therefore can be ignored here - boolean ignoreProxyDownEvent = event.getAction() == MotionEvent.ACTION_DOWN - && mOverviewEventSender.getProxy() != null; - boolean result = mStatusBar.isPresenterFullyCollapsed() - && (mQuickScrubController.onTouchEvent(event) - || ignoreProxyDownEvent - || proxyMotionEvents(event)); - if (mDockWindowEnabled) { - result |= handleDockWindowEvent(event); - } - return result; - } - @Override public void onTuningChanged(String key, String newValue) { switch (key) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java index 9d20e4e1bb87..989423530599 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java @@ -57,13 +57,12 @@ public class NavigationBarInflaterView extends FrameLayout public static final String NAV_BAR_LEFT = "sysui_nav_bar_left"; public static final String NAV_BAR_RIGHT = "sysui_nav_bar_right"; - public static final String MENU_IME = "menu_ime"; + public static final String MENU_IME_ROTATE = "menu_ime"; public static final String BACK = "back"; public static final String HOME = "home"; public static final String RECENT = "recent"; public static final String NAVSPACE = "space"; public static final String CLIPBOARD = "clipboard"; - public static final String ROTATE = "rotate"; public static final String KEY = "key"; public static final String LEFT = "left"; public static final String RIGHT = "right"; @@ -317,10 +316,10 @@ public class NavigationBarInflaterView extends FrameLayout View v = null; String button = extractButton(buttonSpec); if (LEFT.equals(button)) { - String s = Dependency.get(TunerService.class).getValue(NAV_BAR_LEFT, ROTATE); + String s = Dependency.get(TunerService.class).getValue(NAV_BAR_LEFT, NAVSPACE); button = extractButton(s); } else if (RIGHT.equals(button)) { - String s = Dependency.get(TunerService.class).getValue(NAV_BAR_RIGHT, MENU_IME); + String s = Dependency.get(TunerService.class).getValue(NAV_BAR_RIGHT, MENU_IME_ROTATE); button = extractButton(s); } // Let plugins go first so they can override a standard view if they want. @@ -334,14 +333,12 @@ public class NavigationBarInflaterView extends FrameLayout v = inflater.inflate(R.layout.back, parent, false); } else if (RECENT.equals(button)) { v = inflater.inflate(R.layout.recent_apps, parent, false); - } else if (MENU_IME.equals(button)) { + } else if (MENU_IME_ROTATE.equals(button)) { v = inflater.inflate(R.layout.menu_ime, parent, false); } else if (NAVSPACE.equals(button)) { v = inflater.inflate(R.layout.nav_key_space, parent, false); } else if (CLIPBOARD.equals(button)) { v = inflater.inflate(R.layout.clipboard, parent, false); - } else if (ROTATE.equals(button)) { - v = inflater.inflate(R.layout.rotate_suggestion, parent, false); } else if (button.startsWith(KEY)) { String uri = extractImage(button); int code = extractKeycode(button); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index c37dd55cc6ff..285980b39020 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -16,6 +16,13 @@ package com.android.systemui.statusbar.phone; +import static android.view.MotionEvent.ACTION_DOWN; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.LayoutTransition; import android.animation.LayoutTransition.TransitionListener; import android.animation.ObjectAnimator; @@ -29,9 +36,11 @@ import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.drawable.AnimatedVectorDrawable; import android.os.Handler; import android.os.Message; import android.os.RemoteException; +import android.os.SystemProperties; import android.support.annotation.ColorInt; import android.util.AttributeSet; import android.util.Log; @@ -49,15 +58,17 @@ import android.widget.FrameLayout; import com.android.settingslib.Utils; import com.android.systemui.Dependency; import com.android.systemui.DockedStackExistsListener; +import com.android.systemui.Interpolators; import com.android.systemui.OverviewProxyService; -import com.android.systemui.OverviewProxyService.OverviewProxyListener; import com.android.systemui.R; import com.android.systemui.RecentsComponent; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.statusbar.phone.NavGesture; import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper; -import com.android.systemui.recents.SwipeUpOnboarding; +import com.android.systemui.recents.RecentsOnboarding; +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.NavigationBarCompat; import com.android.systemui.stackdivider.Divider; import com.android.systemui.statusbar.policy.DeadZone; import com.android.systemui.statusbar.policy.KeyButtonDrawable; @@ -67,10 +78,17 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.function.Consumer; +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_QUICK_SCRUB; +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_SWIPE_UP; +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_HIDE_BACK_BUTTON; +import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_SHOW_OVERVIEW_BUTTON; + public class NavigationBarView extends FrameLayout implements PluginListener<NavGesture> { final static boolean DEBUG = false; final static String TAG = "StatusBar/NavBarView"; + final static int BUTTON_FADE_IN_OUT_DURATION_MS = 100; + // slippery nav bar when everything is disabled, e.g. during setup final static boolean SLIPPERY_WHEN_DISABLED = true; @@ -86,9 +104,15 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav boolean mShowMenu; boolean mShowAccessibilityButton; boolean mLongClickableAccessibilityButton; + boolean mShowRotateButton; int mDisabledFlags = 0; int mNavigationIconHints = 0; + private @NavigationBarCompat.HitTarget int mDownHitTarget = HIT_TARGET_NONE; + private Rect mHomeButtonBounds = new Rect(); + private Rect mBackButtonBounds = new Rect(); + private int[] mTmpPosition = new int[2]; + private KeyButtonDrawable mBackIcon, mBackLandIcon, mBackAltIcon, mBackAltLandIcon; private KeyButtonDrawable mBackCarModeIcon, mBackLandCarModeIcon; private KeyButtonDrawable mBackAltCarModeIcon, mBackAltLandCarModeIcon; @@ -104,7 +128,6 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav private DeadZone mDeadZone; private final NavigationBarTransitions mBarTransitions; private final OverviewProxyService mOverviewProxyService; - private boolean mRecentsAnimationStarted; // workaround for LayoutTransitions leaving the nav buttons in a weird state (bug 5549288) final static boolean WORKAROUND_INVALID_LAYOUT = true; @@ -126,9 +149,11 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav private NavigationBarInflaterView mNavigationInflaterView; private RecentsComponent mRecentsComponent; private Divider mDivider; - private SwipeUpOnboarding mSwipeUpOnboarding; + private RecentsOnboarding mRecentsOnboarding; private NotificationPanelView mPanelView; + private Animator mRotateHideAnimator; + private class NavTransitionListener implements TransitionListener { private boolean mBackTransitioning; private boolean mHomeAppearing; @@ -219,6 +244,9 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mShowAccessibilityButton = false; mLongClickableAccessibilityButton = false; + mOverviewProxyService = Dependency.get(OverviewProxyService.class); + mRecentsOnboarding = new RecentsOnboarding(context, mOverviewProxyService); + mConfiguration = new Configuration(); mConfiguration.updateFrom(context.getResources().getConfiguration()); updateIcons(context, Configuration.EMPTY, mConfiguration); @@ -234,9 +262,6 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav new ButtonDispatcher(R.id.accessibility_button)); mButtonDispatchers.put(R.id.rotate_suggestion, new ButtonDispatcher(R.id.rotate_suggestion)); - - mOverviewProxyService = Dependency.get(OverviewProxyService.class); - mSwipeUpOnboarding = new SwipeUpOnboarding(context); } public BarTransitions getBarTransitions() { @@ -264,9 +289,8 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav } public void setRecentsAnimationStarted(boolean started) { - mRecentsAnimationStarted = started; - if (mSwipeUpOnboarding != null) { - mSwipeUpOnboarding.onRecentsAnimationStarted(); + if (mRecentsOnboarding != null) { + mRecentsOnboarding.onRecentsAnimationStarted(); } } @@ -277,30 +301,32 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav } @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + switch (event.getActionMasked()) { + case ACTION_DOWN: + int x = (int) event.getX(); + int y = (int) event.getY(); + mDownHitTarget = HIT_TARGET_NONE; + if (mBackButtonBounds.contains(x, y)) { + mDownHitTarget = HIT_TARGET_BACK; + } else if (mHomeButtonBounds.contains(x, y)) { + mDownHitTarget = HIT_TARGET_HOME; + } + break; + } + return mGestureHelper.onInterceptTouchEvent(event); + } + + @Override public boolean onTouchEvent(MotionEvent event) { if (mGestureHelper.onTouchEvent(event)) { return true; } - return mRecentsAnimationStarted || super.onTouchEvent(event); + return super.onTouchEvent(event); } - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - int action = event.getActionMasked(); - if (action == MotionEvent.ACTION_DOWN) { - mRecentsAnimationStarted = false; - } else if (action == MotionEvent.ACTION_UP) { - // If the overview proxy service has not started the recents animation then clean up - // after it to ensure that the nav bar buttons still work - if (mOverviewProxyService.getProxy() != null && !mRecentsAnimationStarted) { - try { - ActivityManager.getService().cancelRecentsAnimation(); - } catch (RemoteException e) { - Log.e(TAG, "Could not cancel recents animation"); - } - } - } - return mGestureHelper.onInterceptTouchEvent(event) || mRecentsAnimationStarted; + public @NavigationBarCompat.HitTarget int getDownHitTarget() { + return mDownHitTarget; } public void abortCurrentGesture() { @@ -353,6 +379,19 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav return getRecentsButton().getVisibility() == View.VISIBLE; } + public boolean isQuickStepSwipeUpEnabled() { + return mOverviewProxyService.getProxy() != null + && ((mOverviewProxyService.getInteractionFlags() + & FLAG_DISABLE_SWIPE_UP) == 0); + } + + public boolean isQuickScrubEnabled() { + return SystemProperties.getBoolean("persist.quickstep.scrub.enabled", true) + && mOverviewProxyService.getProxy() != null && !isRecentsButtonVisible() + && ((mOverviewProxyService.getInteractionFlags() + & FLAG_DISABLE_QUICK_SCRUB) == 0); + } + private void updateCarModeIcons(Context ctx) { mBackCarModeIcon = getDrawable(ctx, R.drawable.ic_sysbar_back_carmode, R.drawable.ic_sysbar_back_carmode); @@ -369,17 +408,14 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav || oldConfig.densityDpi != newConfig.densityDpi) { mDockedIcon = getDrawable(ctx, R.drawable.ic_sysbar_docked, R.drawable.ic_sysbar_docked_dark); + mHomeDefaultIcon = getHomeDrawable(ctx); } if (oldConfig.densityDpi != newConfig.densityDpi || oldConfig.getLayoutDirection() != newConfig.getLayoutDirection()) { - mBackIcon = getDrawable(ctx, R.drawable.ic_sysbar_back, R.drawable.ic_sysbar_back_dark); + mBackIcon = getBackDrawable(ctx); mBackLandIcon = mBackIcon; - mBackAltIcon = getDrawable(ctx, - R.drawable.ic_sysbar_back_ime, R.drawable.ic_sysbar_back_ime_dark); + mBackAltIcon = getBackImeDrawable(ctx); mBackAltLandIcon = mBackAltIcon; - - mHomeDefaultIcon = getDrawable(ctx, - R.drawable.ic_sysbar_home, R.drawable.ic_sysbar_home_dark); mRecentIcon = getDrawable(ctx, R.drawable.ic_sysbar_recent, R.drawable.ic_sysbar_recent_dark); mMenuIcon = getDrawable(ctx, R.drawable.ic_sysbar_menu, R.drawable.ic_sysbar_menu_dark); @@ -404,6 +440,33 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav } } + public KeyButtonDrawable getBackDrawable(Context ctx) { + return chooseNavigationIconDrawable(ctx, R.drawable.ic_sysbar_back, + R.drawable.ic_sysbar_back_dark, R.drawable.ic_sysbar_back_quick_step, + R.drawable.ic_sysbar_back_quick_step_dark); + } + + public KeyButtonDrawable getBackImeDrawable(Context ctx) { + return chooseNavigationIconDrawable(ctx, R.drawable.ic_sysbar_back_ime, + R.drawable.ic_sysbar_back_ime_dark, R.drawable.ic_sysbar_back_ime_quick_step, + R.drawable.ic_sysbar_back_ime_quick_step_dark); + } + + public KeyButtonDrawable getHomeDrawable(Context ctx) { + return chooseNavigationIconDrawable(ctx, R.drawable.ic_sysbar_home, + R.drawable.ic_sysbar_home_dark, R.drawable.ic_sysbar_home_quick_step, + R.drawable.ic_sysbar_home_quick_step_dark); + } + + private KeyButtonDrawable chooseNavigationIconDrawable(Context ctx, @DrawableRes int iconLight, + @DrawableRes int iconDark, @DrawableRes int quickStepIconLight, + @DrawableRes int quickStepIconDark) { + final boolean quickStepEnabled = isQuickStepSwipeUpEnabled() || isQuickScrubEnabled(); + return quickStepEnabled + ? getDrawable(ctx, quickStepIconLight, quickStepIconDark) + : getDrawable(ctx, iconLight, iconDark); + } + private KeyButtonDrawable getDrawable(Context ctx, @DrawableRes int lightIcon, @DrawableRes int darkIcon) { return getDrawable(ctx, ctx, lightIcon, darkIcon); @@ -479,22 +542,25 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav getHomeButton().setImageDrawable(mHomeDefaultIcon); } - // The Accessibility button always overrides the appearance of the IME switcher + // Update IME button visibility, a11y and rotate button always overrides the appearance final boolean showImeButton = - !mShowAccessibilityButton && ((hints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) - != 0); + !mShowAccessibilityButton && + !mShowRotateButton && + ((hints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) != 0); getImeSwitchButton().setVisibility(showImeButton ? View.VISIBLE : View.INVISIBLE); getImeSwitchButton().setImageDrawable(mImeIcon); - // Update menu button in case the IME state has changed. + // Update menu button, visibility logic in method setMenuVisibility(mShowMenu, true); getMenuButton().setImageDrawable(mMenuIcon); + // Update rotate button, visibility altered by a11y button logic + getRotateSuggestionButton().setImageDrawable(mRotateSuggestionIcon); + + // Update a11y button, visibility logic in state method setAccessibilityButtonState(mShowAccessibilityButton, mLongClickableAccessibilityButton); getAccessibilityButton().setImageDrawable(mAccessibilityIcon); - getRotateSuggestionButton().setImageDrawable(mRotateSuggestionIcon); - setDisabledFlags(mDisabledFlags, true); mBarTransitions.reapplyDarkIntensity(); @@ -509,7 +575,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mDisabledFlags = disabledFlags; - final boolean disableHome = ((disabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0); + boolean disableHome = ((disabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0); // Always disable recents when alternate car mode UI is active. boolean disableRecent = mUseCarModeUi @@ -518,15 +584,21 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav boolean disableBack = ((disabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0) && ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) == 0); - if ((disableRecent || disableBack) && inScreenPinning()) { - // Don't hide back and recents buttons when in screen pinning mode, as they are used for - // exiting. - disableBack = false; - disableRecent = false; - } + // When screen pinning, don't hide back and home when connected service or back and + // recents buttons when disconnected from launcher service in screen pinning mode, + // as they are used for exiting. if (mOverviewProxyService.getProxy() != null) { - // When overview is connected to the launcher service, disable the recents button - disableRecent = true; + // Use interaction flags to show/hide navigation buttons but will be shown if required + // to exit screen pinning. + final int flags = mOverviewProxyService.getInteractionFlags(); + disableRecent |= (flags & FLAG_SHOW_OVERVIEW_BUTTON) == 0; + if (inScreenPinning()) { + disableBack = disableHome = false; + } else { + disableBack |= (flags & FLAG_HIDE_BACK_BUTTON) != 0; + } + } else if (inScreenPinning()) { + disableBack = disableRecent = false; } ViewGroup navButtons = (ViewGroup) getCurrentView().findViewById(R.id.nav_buttons); @@ -544,13 +616,8 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE); } - private boolean inScreenPinning() { - try { - return ActivityManager.getService().getLockTaskModeState() - == ActivityManager.LOCK_TASK_MODE_PINNED; - } catch (RemoteException e) { - return false; - } + public boolean inScreenPinning() { + return ActivityManagerWrapper.getInstance().isLockToAppActive(); } public void setLayoutTransitionsEnabled(boolean enabled) { @@ -604,6 +671,11 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav updateSlippery(); } + public void updateStates() { + updateSlippery(); + setDisabledFlags(mDisabledFlags, true); + } + private void updateSlippery() { setSlippery(mOverviewProxyService.getProxy() != null && mPanelView.isFullyExpanded()); } @@ -638,8 +710,10 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mShowMenu = show; - // Only show Menu if IME switcher and Accessibility button not shown. - final boolean shouldShow = mShowMenu && !mShowAccessibilityButton && + // Only show Menu if IME switcher, rotate and Accessibility buttons are not shown. + final boolean shouldShow = mShowMenu && + !mShowAccessibilityButton && + !mShowRotateButton && ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN) == 0); getMenuButton().setVisibility(shouldShow ? View.VISIBLE : View.INVISIBLE); @@ -649,15 +723,96 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav mShowAccessibilityButton = visible; mLongClickableAccessibilityButton = longClickable; if (visible) { - // Accessibility button overrides Menu and IME switcher buttons. + // Accessibility button overrides Menu, IME switcher and rotate buttons. setMenuVisibility(false, true); getImeSwitchButton().setVisibility(View.INVISIBLE); + setRotateSuggestionButtonState(false, true); } getAccessibilityButton().setVisibility(visible ? View.VISIBLE : View.INVISIBLE); getAccessibilityButton().setLongClickable(longClickable); } + public void setRotateSuggestionButtonState(final boolean visible) { + setRotateSuggestionButtonState(visible, false); + } + + public void setRotateSuggestionButtonState(final boolean visible, final boolean force) { + ButtonDispatcher rotBtn = getRotateSuggestionButton(); + final boolean currentlyVisible = mShowRotateButton; + + // Rerun a show animation to indicate change but don't rerun a hide animation + if (!visible && !currentlyVisible) return; + + View currentView = rotBtn.getCurrentView(); + if (currentView == null) return; + + KeyButtonDrawable kbd = rotBtn.getImageDrawable(); + if (kbd == null) return; + + AnimatedVectorDrawable animIcon = null; + if (kbd.getDrawable(0) instanceof AnimatedVectorDrawable) { + animIcon = (AnimatedVectorDrawable) kbd.getDrawable(0); + } + + if (visible) { // Appear and change, cannot force + setRotateButtonVisibility(true); + + // Stop any currently running hide animations + if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) { + mRotateHideAnimator.pause(); + } + + // Reset the alpha if any has changed due to hide animation + currentView.setAlpha(1f); + + // Run the rotate icon's animation if it has one + if (animIcon != null) { + animIcon.reset(); + animIcon.start(); + } + + } else { // Hide + if (force) { + // If a hide animator is running stop it and instantly make invisible + if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) { + mRotateHideAnimator.pause(); + } + setRotateButtonVisibility(false); + return; + } + + // Don't start any new hide animations if one is running + if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return; + + ObjectAnimator fadeOut = ObjectAnimator.ofFloat(currentView, "alpha", + 0f); + fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS); + fadeOut.setInterpolator(Interpolators.LINEAR); + fadeOut.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setRotateButtonVisibility(false); + } + }); + + mRotateHideAnimator = fadeOut; + fadeOut.start(); + } + } + + private void setRotateButtonVisibility(final boolean visible) { + // Never show if a11y is visible + final boolean adjVisible = visible && !mShowAccessibilityButton; + final int vis = adjVisible ? View.VISIBLE : View.INVISIBLE; + + getRotateSuggestionButton().setVisibility(vis); + mShowRotateButton = visible; + + // Hide/restore other button visibility, if necessary + setNavigationIconHints(mNavigationIconHints, true); + } + @Override public void onFinishInflate() { mNavigationInflaterView = (NavigationBarInflaterView) findViewById( @@ -674,15 +829,16 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav if (mGestureHelper != null) { mGestureHelper.onDarkIntensityChange(intensity); } - if (mSwipeUpOnboarding != null) { - mSwipeUpOnboarding.setContentDarkIntensity(intensity); + if (mRecentsOnboarding != null) { + mRecentsOnboarding.setContentDarkIntensity(intensity); } } public void onOverviewProxyConnectionChanged(boolean isConnected) { - setSlippery(!isConnected); - setDisabledFlags(mDisabledFlags, true); - setUpSwipeUpOnboarding(isConnected); + updateStates(); + setUpSwipeUpOnboarding(isQuickStepSwipeUpEnabled()); + updateIcons(getContext(), Configuration.EMPTY, mConfiguration); + setNavigationIconHints(mNavigationIconHints, true); } @Override @@ -694,9 +850,23 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); + updateButtonLocationOnScreen(getBackButton(), mBackButtonBounds); + updateButtonLocationOnScreen(getHomeButton(), mHomeButtonBounds); mGestureHelper.onLayout(changed, left, top, right, bottom); } + private void updateButtonLocationOnScreen(ButtonDispatcher button, Rect buttonBounds) { + View view = button.getCurrentView(); + if (view == null) { + buttonBounds.setEmpty(); + return; + } + view.getLocationInWindow(mTmpPosition); + buttonBounds.set(mTmpPosition[0], mTmpPosition[1], + mTmpPosition[0] + view.getMeasuredWidth(), + mTmpPosition[1] + view.getMeasuredHeight()); + } + private void updateRotatedViews() { mRotatedViews[Surface.ROTATION_0] = mRotatedViews[Surface.ROTATION_180] = findViewById(R.id.rot0); @@ -751,6 +921,11 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav Log.d(TAG, "reorient(): rot=" + mCurrentRotation); } + // Resolve layout direction if not resolved since components changing layout direction such + // as changing languages will recreate this view and the direction will be resolved later + if (!isLayoutDirectionResolved()) { + resolveLayoutDirection(); + } updateTaskSwitchHelper(); setNavigationIconHints(mNavigationIconHints, true); @@ -793,7 +968,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav updateTaskSwitchHelper(); updateIcons(getContext(), mConfiguration, newConfig); updateRecentsIcon(); - mSwipeUpOnboarding.onConfigurationChanged(newConfig); + mRecentsOnboarding.onConfigurationChanged(newConfig); if (uiCarModeChanged || mConfiguration.densityDpi != newConfig.densityDpi || mConfiguration.getLayoutDirection() != newConfig.getLayoutDirection()) { // If car mode or density changes, we need to reset the icons. @@ -897,9 +1072,9 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav private void setUpSwipeUpOnboarding(boolean connectedToOverviewProxy) { if (connectedToOverviewProxy) { - mSwipeUpOnboarding.onConnectedToLauncher(); + mRecentsOnboarding.onConnectedToLauncher(); } else { - mSwipeUpOnboarding.onDisconnectedFromLauncher(); + mRecentsOnboarding.onDisconnectedFromLauncher(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java index cd2e77ae2591..3b129fc5e08d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -45,7 +45,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowInsets; -import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import com.android.internal.logging.MetricsLogger; @@ -68,14 +68,12 @@ import com.android.systemui.statusbar.NotificationData; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.ActivityLaunchAnimator; -import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardUserSwitcher; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.stack.StackStateAnimator; import java.util.List; -import java.util.Collection; public class NotificationPanelView extends PanelView implements ExpandableView.OnHeightChangedListener, @@ -113,6 +111,7 @@ public class NotificationPanelView extends PanelView implements } }; private final PowerManager mPowerManager; + private final AccessibilityManager mAccessibilityManager; private KeyguardAffordanceHelper mAffordanceHelper; private KeyguardUserSwitcher mKeyguardUserSwitcher; @@ -251,6 +250,8 @@ public class NotificationPanelView extends PanelView implements setWillNotDraw(!DEBUG); mFalsingManager = FalsingManager.getInstance(context); mPowerManager = context.getSystemService(PowerManager.class); + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); + setAccessibilityPaneTitle(determineAccessibilityPaneTitle()); } public void setStatusBar(StatusBar bar) { @@ -500,7 +501,8 @@ public class NotificationPanelView extends PanelView implements float shelfSize = shelf.getVisibility() == GONE ? 0 : shelf.getIntrinsicHeight() + notificationPadding; float availableSpace = mNotificationStackScroller.getHeight() - minPadding - shelfSize - - Math.max(mIndicationBottomPadding, mAmbientIndicationBottomPadding); + - Math.max(mIndicationBottomPadding, mAmbientIndicationBottomPadding) + - mKeyguardStatusView.getLogoutButtonHeight(); int count = 0; for (int i = 0; i < mNotificationStackScroller.getChildCount(); i++) { ExpandableView child = (ExpandableView) mNotificationStackScroller.getChildAt(i); @@ -663,16 +665,6 @@ public class NotificationPanelView extends PanelView implements } @Override - public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { - if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { - event.getText().add(getKeyguardOrLockScreenString()); - mLastAnnouncementWasQuickSettings = false; - return true; - } - return super.dispatchPopulateAccessibilityEventInternal(event); - } - - @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (mBlockTouches || mQsFullyExpanded && mQs.onInterceptTouchEvent(event)) { return false; @@ -1302,10 +1294,6 @@ public class NotificationPanelView extends PanelView implements setQsExpanded(true); } else if (height <= mQsMinExpansionHeight && mQsExpanded) { setQsExpanded(false); - if (mLastAnnouncementWasQuickSettings && !mTracking && !isCollapsing()) { - announceForAccessibility(getKeyguardOrLockScreenString()); - mLastAnnouncementWasQuickSettings = false; - } } mQsExpansionHeight = height; updateQsExpansion(); @@ -1331,13 +1319,10 @@ public class NotificationPanelView extends PanelView implements updateClock(mClockPositionResult.clockAlpha, mClockPositionResult.clockScale); } - // Upon initialisation when we are not layouted yet we don't want to announce that we are - // fully expanded, hence the != 0.0f check. - if (height != 0.0f && mQsFullyExpanded && !mLastAnnouncementWasQuickSettings) { - announceForAccessibility(getContext().getString( - R.string.accessibility_desc_quick_settings)); - mLastAnnouncementWasQuickSettings = true; + if (mAccessibilityManager.isEnabled()) { + setAccessibilityPaneTitle(determineAccessibilityPaneTitle()); } + if (mQsFullyExpanded && mFalsingManager.shouldEnforceBouncer()) { mStatusBar.executeRunnableDismissingKeyguard(null, null /* cancelAction */, false /* dismissShade */, true /* afterKeyguardGone */, false /* deferred */); @@ -1352,9 +1337,13 @@ public class NotificationPanelView extends PanelView implements mQs.setQsExpansion(getQsExpansionFraction(), getHeaderTranslation()); } - private String getKeyguardOrLockScreenString() { + private String determineAccessibilityPaneTitle() { if (mQs != null && mQs.isCustomizing()) { return getContext().getString(R.string.accessibility_desc_quick_settings_edit); + } else if (mQsExpansionHeight != 0.0f && mQsFullyExpanded) { + // Upon initialisation when we are not layouted yet we don't want to announce that we + // are fully expanded, hence the != 0.0f check. + return getContext().getString(R.string.accessibility_desc_quick_settings); } else if (mStatusBarState == StatusBarState.KEYGUARD) { return getContext().getString(R.string.accessibility_desc_lock_screen); } else { @@ -1571,7 +1560,7 @@ public class NotificationPanelView extends PanelView implements private void updatePanelExpanded() { boolean isExpanded = !isFullyCollapsed(); if (mPanelExpanded != isExpanded) { - mHeadsUpManager.setIsExpanded(isExpanded); + mHeadsUpManager.setIsPanelExpanded(isExpanded); mStatusBar.setPanelExpanded(isExpanded); mPanelExpanded = isExpanded; } @@ -1882,6 +1871,9 @@ public class NotificationPanelView extends PanelView implements requestScrollerTopPaddingUpdate(false /* animate */); requestPanelHeightUpdate(); } + if (mAccessibilityManager.isEnabled()) { + setAccessibilityPaneTitle(determineAccessibilityPaneTitle()); + } } @Override @@ -2338,7 +2330,7 @@ public class NotificationPanelView extends PanelView implements } @Override - public void setHeadsUpManager(HeadsUpManager headsUpManager) { + public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) { super.setHeadsUpManager(headsUpManager); mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager, mNotificationStackScroller, this); @@ -2630,8 +2622,8 @@ public class NotificationPanelView extends PanelView implements } } - public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing) { - mKeyguardStatusView.setPulsing(pulsing != null); + public void setPulsing(boolean pulsing) { + mKeyguardStatusView.setPulsing(pulsing); positionClockAndNotifications(); mNotificationStackScroller.setPulsing(pulsing, mKeyguardStatusView.getLocationOnScreen()[1] + mKeyguardStatusView.getClockBottom()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java index b6c7d74a33d1..dbabdb2a32dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java @@ -50,8 +50,8 @@ import com.android.systemui.classifier.FalsingManager; import com.android.systemui.doze.DozeLog; import com.android.systemui.statusbar.FlingAnimationUtils; import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.policy.HeadsUpManager; import android.util.BoostFramework; +import com.android.systemui.statusbar.phone.HeadsUpManagerPhone; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -76,7 +76,7 @@ public abstract class PanelView extends FrameLayout { } protected StatusBar mStatusBar; - protected HeadsUpManager mHeadsUpManager; + protected HeadsUpManagerPhone mHeadsUpManager; private float mPeekHeight; private float mHintDistance; @@ -1270,7 +1270,7 @@ public abstract class PanelView extends FrameLayout { */ protected abstract int getClearAllHeight(); - public void setHeadsUpManager(HeadsUpManager headsUpManager) { + public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) { mHeadsUpManager = headsUpManager; } 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 6444cc816663..747a551defe6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -575,7 +575,7 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks, Intent browserIntent = getTaskIntent(taskId, userId); Notification.Builder builder = new Notification.Builder(mContext, NotificationChannels.GENERAL); - if (browserIntent != null) { + if (browserIntent != null && browserIntent.isWebIntent()) { // Make sure that this doesn't resolve back to an instant app browserIntent.setComponent(null) .setPackage(null) @@ -597,8 +597,9 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks, .addCategory("unique:" + System.currentTimeMillis()) .putExtra(Intent.EXTRA_PACKAGE_NAME, appInfo.packageName) .putExtra(Intent.EXTRA_VERSION_CODE, (int) (appInfo.versionCode & 0x7fffffff)) - .putExtra(Intent.EXTRA_VERSION_CODE, appInfo.versionCode) - .putExtra(Intent.EXTRA_EPHEMERAL_FAILURE, pendingIntent); + .putExtra(Intent.EXTRA_LONG_VERSION_CODE, appInfo.versionCode) + .putExtra(Intent.EXTRA_EPHEMERAL_FAILURE, pendingIntent) + .putExtra(Intent.EXTRA_INSTANT_APP_FAILURE, pendingIntent); PendingIntent webPendingIntent = PendingIntent.getActivity(mContext, 0, goToWebIntent, 0); Action webAction = new Notification.Action.Builder(null, mContext.getString(R.string.go_to_web), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubController.java index 001a1a2d7292..378858a9b816 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickScrubController.java @@ -29,15 +29,12 @@ import android.graphics.Paint; import android.graphics.Rect; import android.os.Handler; import android.os.RemoteException; -import android.os.SystemProperties; import android.util.Log; import android.util.Slog; -import android.view.Display; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; -import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; @@ -53,6 +50,7 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT; import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM; import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY; import static com.android.systemui.OverviewProxyService.TAG_OPS; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME; /** * Class to detect gestures on the navigation bar and implement quick scrub and switch. @@ -63,7 +61,7 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene private static final String TAG = "QuickScrubController"; private static final int QUICK_SWITCH_FLING_VELOCITY = 0; private static final int ANIM_DURATION_MS = 200; - private static final long LONG_PRESS_DELAY_MS = 150; + private static final long LONG_PRESS_DELAY_MS = 225; /** * For quick step, set a damping value to allow the button to stick closer its origin position @@ -76,6 +74,7 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene private boolean mDraggingActive; private boolean mQuickScrubActive; + private boolean mAllowQuickSwitch; private float mDownOffset; private float mTranslation; private int mTouchDownX; @@ -87,15 +86,14 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene private int mLightTrackColor; private int mDarkTrackColor; private float mDarkIntensity; + private View mHomeButtonView; private final Handler mHandler = new Handler(); private final Interpolator mQuickScrubEndInterpolator = new DecelerateInterpolator(); private final Rect mTrackRect = new Rect(); - private final Rect mHomeButtonRect = new Rect(); private final Paint mTrackPaint = new Paint(); private final int mScrollTouchSlop; private final OverviewProxyService mOverviewEventSender; - private final Display mDisplay; private final int mTrackThickness; private final int mTrackPadding; private final ValueAnimator mTrackAnimator; @@ -114,11 +112,10 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene if (!mQuickScrubActive) { pos = mDragPositive ? Math.min((int) mTranslation, pos) : Math.max((int) mTranslation, pos); } - final View homeView = mNavigationBarView.getHomeButton().getCurrentView(); if (mIsVertical) { - homeView.setTranslationY(pos); + mHomeButtonView.setTranslationY(pos); } else { - homeView.setTranslationX(pos); + mHomeButtonView.setTranslationX(pos); } }; @@ -126,6 +123,7 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene @Override public void onAnimationEnd(Animator animation) { mNavigationBarView.getHomeButton().setClickable(true); + mHomeButtonView = null; mQuickScrubActive = false; mTranslation = 0; } @@ -137,7 +135,9 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene new GestureDetector.SimpleOnGestureListener() { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velX, float velY) { - if (!isQuickScrubEnabled() || mQuickScrubActive) { + if (!mNavigationBarView.isQuickScrubEnabled() || mQuickScrubActive + || !mAllowQuickSwitch + || mNavigationBarView.getDownHitTarget() != HIT_TARGET_HOME) { return false; } float velocityX = mIsRTL ? -velX : velX; @@ -158,16 +158,15 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene } catch (RemoteException e) { Log.e(TAG, "Failed to send start of quick switch.", e); } + return true; } - return true; + return false; } }; public QuickScrubController(Context context) { mContext = context; mScrollTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); - mDisplay = ((WindowManager) context.getSystemService( - Context.WINDOW_SERVICE)).getDefaultDisplay(); mOverviewEventSender = Dependency.get(OverviewProxyService.class); mGestureDetector = new GestureDetector(mContext, mGestureListener); mTrackThickness = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_thickness); @@ -189,21 +188,49 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene mNavigationBarView = navigationBarView; } + /** + * @return true if we want to intercept touch events for quick scrub/switch and prevent proxying + * the event to the overview service. + */ @Override public boolean onInterceptTouchEvent(MotionEvent event) { - final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy(); final ButtonDispatcher homeButton = mNavigationBarView.getHomeButton(); - if (overviewProxy == null) { + if (!mNavigationBarView.isQuickScrubEnabled()) { homeButton.setDelayTouchFeedback(false); return false; } - mGestureDetector.onTouchEvent(event); + + return handleTouchEvent(event); + } + + /** + * @return true if we want to handle touch events for quick scrub/switch and prevent proxying + * the event to the overview service. + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + return handleTouchEvent(event); + } + + private boolean handleTouchEvent(MotionEvent event) { + final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy(); + final ButtonDispatcher homeButton = mNavigationBarView.getHomeButton(); + if (mGestureDetector.onTouchEvent(event)) { + // If the fling has been handled on UP, then skip proxying the UP + return true; + } int action = event.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { int x = (int) event.getX(); int y = (int) event.getY(); - if (isQuickScrubEnabled() && mHomeButtonRect.contains(x, y)) { + // End any existing quickscrub animations before starting the new transition + if (mQuickScrubEndAnimator != null) { + mQuickScrubEndAnimator.end(); + } + mHomeButtonView = homeButton.getCurrentView(); + if (mNavigationBarView.isQuickScrubEnabled() + && mNavigationBarView.getDownHitTarget() == HIT_TARGET_HOME) { mTouchDownX = x; mTouchDownY = y; homeButton.setDelayTouchFeedback(true); @@ -212,6 +239,7 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene homeButton.setDelayTouchFeedback(false); mTouchDownX = mTouchDownY = -1; } + mAllowQuickSwitch = true; break; } case MotionEvent.ACTION_MOVE: { @@ -240,8 +268,9 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene offset = pos - mTrackRect.left; trackSize = mTrackRect.width(); } - // Do not start scrubbing when dragging in the perpendicular direction - if (!mDraggingActive && exceededPerpendicularTouchSlop) { + // Do not start scrubbing when dragging in the perpendicular direction if we + // haven't already started quickscrub + if (!mDraggingActive && !mQuickScrubActive && exceededPerpendicularTouchSlop) { mHandler.removeCallbacksAndMessages(null); return false; } @@ -268,7 +297,7 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene : Utilities.clamp(offset - mDownOffset, 0, trackSize); if (mQuickScrubActive) { try { - overviewProxy.onQuickScrubProgress(scrubFraction); + mOverviewEventSender.getProxy().onQuickScrubProgress(scrubFraction); if (DEBUG_OVERVIEW_PROXY) { Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction); } @@ -279,9 +308,9 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene mTranslation /= SWITCH_STICKINESS; } if (mIsVertical) { - homeButton.getCurrentView().setTranslationY(mTranslation); + mHomeButtonView.setTranslationY(mTranslation); } else { - homeButton.getCurrentView().setTranslationX(mTranslation); + mHomeButtonView.setTranslationX(mTranslation); } } } @@ -289,7 +318,7 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: - endQuickScrub(); + endQuickScrub(true /* animate */); break; } return mDraggingActive || mQuickScrubActive; @@ -321,17 +350,6 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene x2 = x1 + width / 2 - mTrackPadding; } mTrackRect.set(x1, y1, x2, y2); - - // Get the touch rect of the home button location - View homeView = mNavigationBarView.getHomeButton().getCurrentView(); - if (homeView != null) { - int[] globalHomePos = homeView.getLocationOnScreen(); - int[] globalNavBarPos = mNavigationBarView.getLocationOnScreen(); - int homeX = globalHomePos[0] - globalNavBarPos[0]; - int homeY = globalHomePos[1] - globalNavBarPos[1]; - mHomeButtonRect.set(homeX, homeY, homeX + homeView.getMeasuredWidth(), - homeY + homeView.getMeasuredHeight()); - } } @Override @@ -341,15 +359,12 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene } @Override - public boolean onTouchEvent(MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_UP) { - endQuickScrub(); - } - return false; - } - - @Override public void setBarState(boolean isVertical, boolean isRTL) { + final boolean changed = (mIsVertical != isVertical) || (mIsRTL != isRTL); + if (changed) { + // End quickscrub if the state changes mid-transition + endQuickScrub(false /* animate */); + } mIsVertical = isVertical; mIsRTL = isRTL; try { @@ -363,10 +378,6 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene } } - boolean isQuickScrubEnabled() { - return SystemProperties.getBoolean("persist.quickstep.scrub.enabled", false); - } - private void startQuickScrub() { if (!mQuickScrubActive) { mQuickScrubActive = true; @@ -385,7 +396,7 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene } } - private void endQuickScrub() { + private void endQuickScrub(boolean animate) { mHandler.removeCallbacks(mLongPressRunnable); if (mDraggingActive || mQuickScrubActive) { mButtonAnimator.setIntValues((int) mTranslation, 0); @@ -399,10 +410,18 @@ public class QuickScrubController extends GestureDetector.SimpleOnGestureListene } catch (RemoteException e) { Log.e(TAG, "Failed to send end of quick scrub.", e); } + if (!animate) { + mQuickScrubEndAnimator.end(); + } } mDraggingActive = false; } + public void cancelQuickSwitch() { + mAllowQuickSwitch = false; + mHandler.removeCallbacks(mLongPressRunnable); + } + private int getDimensionPixelSize(Context context, @DimenRes int resId) { return context.getResources().getDimensionPixelSize(resId); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 59c44ed6635f..2b5085389804 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -30,6 +30,7 @@ import android.os.Handler; import android.os.Trace; import android.util.Log; import android.util.MathUtils; +import android.view.Choreographer; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; @@ -111,7 +112,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, protected static final float SCRIM_IN_FRONT_ALPHA_LOCKED = GRADIENT_SCRIM_ALPHA_BUSY; static final int TAG_KEY_ANIM = R.id.scrim; - static final int TAG_KEY_ANIM_BLANK = R.id.scrim_blanking; private static final int TAG_KEY_ANIM_TARGET = R.id.scrim_target; private static final int TAG_START_ALPHA = R.id.scrim_alpha_start; private static final int TAG_END_ALPHA = R.id.scrim_alpha_end; @@ -166,6 +166,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, private boolean mScreenBlankingCallbackCalled; private Callback mCallback; private boolean mWallpaperSupportsAmbientMode; + private boolean mScreenOn; + + // Scrim blanking callbacks + private Choreographer.FrameCallback mPendingFrameCallback; + private Runnable mBlankingTransitionRunnable; private final WakeLock mWakeLock; private boolean mWakeLockHeld; @@ -248,6 +253,16 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, mCurrentInFrontAlpha = state.getFrontAlpha(); mCurrentBehindAlpha = state.getBehindAlpha(); + // Cancel blanking transitions that were pending before we requested a new state + if (mPendingFrameCallback != null) { + Choreographer.getInstance().removeFrameCallback(mPendingFrameCallback); + mPendingFrameCallback = null; + } + if (getHandler().hasCallbacks(mBlankingTransitionRunnable)) { + getHandler().removeCallbacks(mBlankingTransitionRunnable); + mBlankingTransitionRunnable = null; + } + // Showing/hiding the keyguard means that scrim colors have to be switched, not necessary // to do the same when you're just showing the brightness mirror. mNeedsDrawableColorUpdate = state != ScrimState.BRIGHTNESS_MIRROR; @@ -692,11 +707,12 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, } } - final boolean blankingInProgress = mScrimInFront.getTag(TAG_KEY_ANIM_BLANK) != null; - if (mBlankScreen || blankingInProgress) { - if (!blankingInProgress) { - blankDisplay(); - } + if (mPendingFrameCallback != null) { + // Display is off and we're waiting. + return; + } else if (mBlankScreen) { + // Need to blank the display before continuing. + blankDisplay(); return; } else if (!mScreenBlankingCallbackCalled) { // Not blanking the screen. Letting the callback know that we're ready @@ -750,45 +766,38 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, } private void blankDisplay() { - final float initialAlpha = mScrimInFront.getViewAlpha(); - final int initialTint = mScrimInFront.getTint(); - ValueAnimator anim = ValueAnimator.ofFloat(0, 1); - anim.addUpdateListener(animation -> { - final float amount = (float) animation.getAnimatedValue(); - float animAlpha = MathUtils.lerp(initialAlpha, 1, amount); - int animTint = ColorUtils.blendARGB(initialTint, Color.BLACK, amount); - updateScrimColor(mScrimInFront, animAlpha, animTint); - dispatchScrimsVisible(); - }); - anim.setInterpolator(getInterpolator()); - anim.setDuration(mDozeParameters.getPulseInDuration()); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (mCallback != null) { - mCallback.onDisplayBlanked(); - mScreenBlankingCallbackCalled = true; - } - Runnable blankingCallback = () -> { - mScrimInFront.setTag(TAG_KEY_ANIM_BLANK, null); - mBlankScreen = false; - // Try again. - updateScrims(); - }; + updateScrimColor(mScrimInFront, 1, Color.BLACK); - // Setting power states can happen after we push out the frame. Make sure we - // stay fully opaque until the power state request reaches the lower levels. - getHandler().postDelayed(blankingCallback, 100); + // Notify callback that the screen is completely black and we're + // ready to change the display power mode + mPendingFrameCallback = frameTimeNanos -> { + if (mCallback != null) { + mCallback.onDisplayBlanked(); + mScreenBlankingCallbackCalled = true; + } + mBlankingTransitionRunnable = () -> { + mBlankingTransitionRunnable = null; + mPendingFrameCallback = null; + mBlankScreen = false; + // Try again. + updateScrims(); + }; + + // Setting power states can happen after we push out the frame. Make sure we + // stay fully opaque until the power state request reaches the lower levels. + final int delay = mScreenOn ? 16 : 500; + if (DEBUG) { + Log.d(TAG, "Fading out scrims with delay: " + delay); } - }); - anim.start(); - mScrimInFront.setTag(TAG_KEY_ANIM_BLANK, anim); + getHandler().postDelayed(mBlankingTransitionRunnable, delay); + }; + doOnTheNextFrame(mPendingFrameCallback); + } - // Finish animation if we're already at its final state - if (initialAlpha == 1 && mScrimInFront.getTint() == Color.BLACK) { - anim.end(); - } + @VisibleForTesting + protected void doOnTheNextFrame(Choreographer.FrameCallback callback) { + Choreographer.getInstance().postFrameCallback(callback); } @VisibleForTesting @@ -893,6 +902,25 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, } } + /** + * Interrupts blanking transitions once the display notifies that it's already on. + */ + public void onScreenTurnedOn() { + mScreenOn = true; + final Handler handler = getHandler(); + if (handler.hasCallbacks(mBlankingTransitionRunnable)) { + if (DEBUG) { + Log.d(TAG, "Shorter blanking because screen turned on. All good."); + } + handler.removeCallbacks(mBlankingTransitionRunnable); + mBlankingTransitionRunnable.run(); + } + } + + public void onScreenTurnedOff() { + mScreenOn = false; + } + public interface Callback { default void onStart() { } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 314d6aa2bf57..381e4af31853 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -48,7 +48,6 @@ public enum ScrimState { // set our scrim to black in this frame to avoid flickering and // fade it out afterwards. mBlankScreen = true; - updateScrimColor(mScrimInFront, 1, Color.BLACK); } } else { mAnimationDuration = ScrimController.ANIMATION_DURATION; @@ -86,9 +85,6 @@ public enum ScrimState { AOD(3) { @Override public void prepare(ScrimState previousState) { - if (previousState == ScrimState.PULSING && !mCanControlScreenOff) { - updateScrimColor(mScrimInFront, 1, Color.BLACK); - } final boolean alwaysOnEnabled = mDozeParameters.getAlwaysOn(); final boolean wasPulsing = previousState == ScrimState.PULSING; mBlankScreen = wasPulsing && !mCanControlScreenOff; @@ -115,9 +111,6 @@ public enum ScrimState { && !mKeyguardUpdateMonitor.hasLockscreenWallpaper() ? 0f : 1f; mCurrentBehindTint = Color.BLACK; mBlankScreen = mDisplayRequiresBlanking; - if (mDisplayRequiresBlanking) { - updateScrimColor(mScrimInFront, 1, Color.BLACK); - } } }, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SignalDrawable.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SignalDrawable.java deleted file mode 100644 index 15ef742af02e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SignalDrawable.java +++ /dev/null @@ -1,517 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.android.systemui.statusbar.phone; - -import android.animation.ArgbEvaluator; -import android.annotation.IntRange; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.Path.Direction; -import android.graphics.Path.FillType; -import android.graphics.Path.Op; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.util.LayoutDirection; - -import com.android.settingslib.R; -import com.android.settingslib.Utils; -import com.android.systemui.qs.SlashDrawable; - -public class SignalDrawable extends Drawable { - - private static final String TAG = "SignalDrawable"; - - private static final int NUM_DOTS = 3; - - private static final float VIEWPORT = 24f; - private static final float PAD = 2f / VIEWPORT; - private static final float CUT_OUT = 7.9f / VIEWPORT; - - private static final float DOT_SIZE = 3f / VIEWPORT; - private static final float DOT_PADDING = 1f / VIEWPORT; - private static final float DOT_CUT_WIDTH = (DOT_SIZE * 3) + (DOT_PADDING * 5); - private static final float DOT_CUT_HEIGHT = (DOT_SIZE * 1) + (DOT_PADDING * 1); - - private static final float[] FIT = {2.26f, -3.02f, 1.76f}; - - // All of these are masks to push all of the drawable state into one int for easy callbacks - // and flow through sysui. - private static final int LEVEL_MASK = 0xff; - private static final int NUM_LEVEL_SHIFT = 8; - private static final int NUM_LEVEL_MASK = 0xff << NUM_LEVEL_SHIFT; - private static final int STATE_SHIFT = 16; - private static final int STATE_MASK = 0xff << STATE_SHIFT; - private static final int STATE_NONE = 0; - private static final int STATE_EMPTY = 1; - private static final int STATE_CUT = 2; - private static final int STATE_CARRIER_CHANGE = 3; - private static final int STATE_AIRPLANE = 4; - - private static final long DOT_DELAY = 1000; - - private static float[][] X_PATH = new float[][]{ - {21.9f / VIEWPORT, 17.0f / VIEWPORT}, - {-1.1f / VIEWPORT, -1.1f / VIEWPORT}, - {-1.9f / VIEWPORT, 1.9f / VIEWPORT}, - {-1.9f / VIEWPORT, -1.9f / VIEWPORT}, - {-1.1f / VIEWPORT, 1.1f / VIEWPORT}, - {1.9f / VIEWPORT, 1.9f / VIEWPORT}, - {-1.9f / VIEWPORT, 1.9f / VIEWPORT}, - {1.1f / VIEWPORT, 1.1f / VIEWPORT}, - {1.9f / VIEWPORT, -1.9f / VIEWPORT}, - {1.9f / VIEWPORT, 1.9f / VIEWPORT}, - {1.1f / VIEWPORT, -1.1f / VIEWPORT}, - {-1.9f / VIEWPORT, -1.9f / VIEWPORT}, - }; - - // Rounded corners are achieved by arcing a circle of radius `R` from its tangent points along - // the curve (curve ≡ triangle). On the top and left corners of the triangle, the tangents are - // as follows: - // 1) Along the straight lines (y = 0 and x = width): - // Ps = circleOffset + R - // 2) Along the diagonal line (y = x): - // Pd = √((Ps^2) / 2) - // or (remember: sin(π/4) ≈ 0.7071) - // Pd = (circleOffset + R - 0.7071, height - R - 0.7071) - // Where Pd is the (x,y) coords of the point that intersects the circle at the bottom - // left of the triangle - private static final float RADIUS_RATIO = 0.75f / 17f; - private static final float DIAG_OFFSET_MULTIPLIER = 0.707107f; - // How far the circle defining the corners is inset from the edges - private final float mAppliedCornerInset; - - private static final float INV_TAN = 1f / (float) Math.tan(Math.PI / 8f); - private static final float CUT_WIDTH_DP = 1f / 12f; - - // Where the top and left points of the triangle would be if not for rounding - private final PointF mVirtualTop = new PointF(); - private final PointF mVirtualLeft = new PointF(); - - private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private final int mDarkModeBackgroundColor; - private final int mDarkModeFillColor; - private final int mLightModeBackgroundColor; - private final int mLightModeFillColor; - private final Path mFullPath = new Path(); - private final Path mForegroundPath = new Path(); - private final Path mXPath = new Path(); - // Cut out when STATE_EMPTY - private final Path mCutPath = new Path(); - // Draws the slash when in airplane mode - private final SlashArtist mSlash = new SlashArtist(); - private final Handler mHandler; - private float mOldDarkIntensity = -1; - private float mNumLevels = 1; - private int mIntrinsicSize; - private int mLevel; - private int mState; - private boolean mVisible; - private boolean mAnimating; - private int mCurrentDot; - - public SignalDrawable(Context context) { - mDarkModeBackgroundColor = - Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_background); - mDarkModeFillColor = - Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_fill); - mLightModeBackgroundColor = - Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_background); - mLightModeFillColor = - Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_fill); - mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size); - - mHandler = new Handler(); - setDarkIntensity(0); - - mAppliedCornerInset = context.getResources() - .getDimensionPixelSize(R.dimen.stat_sys_mobile_signal_circle_inset); - } - - public void setIntrinsicSize(int size) { - mIntrinsicSize = size; - } - - @Override - public int getIntrinsicWidth() { - return mIntrinsicSize; - } - - @Override - public int getIntrinsicHeight() { - return mIntrinsicSize; - } - - public void setNumLevels(int levels) { - if (levels == mNumLevels) return; - mNumLevels = levels; - invalidateSelf(); - } - - private void setSignalState(int state) { - if (state == mState) return; - mState = state; - updateAnimation(); - invalidateSelf(); - } - - private void updateAnimation() { - boolean shouldAnimate = (mState == STATE_CARRIER_CHANGE) && mVisible; - if (shouldAnimate == mAnimating) return; - mAnimating = shouldAnimate; - if (shouldAnimate) { - mChangeDot.run(); - } else { - mHandler.removeCallbacks(mChangeDot); - } - } - - @Override - protected boolean onLevelChange(int state) { - setNumLevels(getNumLevels(state)); - setSignalState(getState(state)); - int level = getLevel(state); - if (level != mLevel) { - mLevel = level; - invalidateSelf(); - } - return true; - } - - public void setColors(int background, int foreground) { - mPaint.setColor(background); - mForegroundPaint.setColor(foreground); - } - - public void setDarkIntensity(float darkIntensity) { - if (darkIntensity == mOldDarkIntensity) { - return; - } - mPaint.setColor(getBackgroundColor(darkIntensity)); - mForegroundPaint.setColor(getFillColor(darkIntensity)); - mOldDarkIntensity = darkIntensity; - invalidateSelf(); - } - - private int getFillColor(float darkIntensity) { - return getColorForDarkIntensity( - darkIntensity, mLightModeFillColor, mDarkModeFillColor); - } - - private int getBackgroundColor(float darkIntensity) { - return getColorForDarkIntensity( - darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor); - } - - private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) { - return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor); - } - - @Override - protected void onBoundsChange(Rect bounds) { - super.onBoundsChange(bounds); - invalidateSelf(); - } - - @Override - public void draw(@NonNull Canvas canvas) { - final float width = getBounds().width(); - final float height = getBounds().height(); - - boolean isRtl = getLayoutDirection() == LayoutDirection.RTL; - if (isRtl) { - canvas.save(); - // Mirror the drawable - canvas.translate(width, 0); - canvas.scale(-1.0f, 1.0f); - } - mFullPath.reset(); - mFullPath.setFillType(FillType.WINDING); - - final float padding = Math.round(PAD * width); - final float cornerRadius = RADIUS_RATIO * height; - // Offset from circle where the hypotenuse meets the circle - final float diagOffset = DIAG_OFFSET_MULTIPLIER * cornerRadius; - - // 1 - Bottom right, above corner - mFullPath.moveTo(width - padding, height - padding - cornerRadius); - // 2 - Line to top right, below corner - mFullPath.lineTo(width - padding, padding + cornerRadius + mAppliedCornerInset); - // 3 - Arc to top right, on hypotenuse - mFullPath.arcTo( - width - padding - (2 * cornerRadius), - padding + mAppliedCornerInset, - width - padding, - padding + mAppliedCornerInset + (2 * cornerRadius), - 0.f, -135.f, false - ); - // 4 - Line to bottom left, on hypotenuse - mFullPath.lineTo(padding + mAppliedCornerInset + cornerRadius - diagOffset, - height - padding - cornerRadius - diagOffset); - // 5 - Arc to bottom left, on leg - mFullPath.arcTo( - padding + mAppliedCornerInset, - height - padding - (2 * cornerRadius), - padding + mAppliedCornerInset + ( 2 * cornerRadius), - height - padding, - -135.f, -135.f, false - ); - // 6 - Line to bottom rght, before corner - mFullPath.lineTo(width - padding - cornerRadius, height - padding); - // 7 - Arc to beginning (bottom right, above corner) - mFullPath.arcTo( - width - padding - (2 * cornerRadius), - height - padding - (2 * cornerRadius), - width - padding, - height - padding, - 90.f, -90.f, false - ); - - if (mState == STATE_CARRIER_CHANGE) { - float cutWidth = (DOT_CUT_WIDTH * width); - float cutHeight = (DOT_CUT_HEIGHT * width); - float dotSize = (DOT_SIZE * height); - float dotPadding = (DOT_PADDING * height); - - mFullPath.moveTo(width - padding, height - padding); - mFullPath.rLineTo(-cutWidth, 0); - mFullPath.rLineTo(0, -cutHeight); - mFullPath.rLineTo(cutWidth, 0); - mFullPath.rLineTo(0, cutHeight); - float dotSpacing = dotPadding * 2 + dotSize; - float x = width - padding - dotSize; - float y = height - padding - dotSize; - mForegroundPath.reset(); - drawDot(mFullPath, mForegroundPath, x, y, dotSize, 2); - drawDot(mFullPath, mForegroundPath, x - dotSpacing, y, dotSize, 1); - drawDot(mFullPath, mForegroundPath, x - dotSpacing * 2, y, dotSize, 0); - } else if (mState == STATE_CUT) { - float cut = (CUT_OUT * width); - mFullPath.moveTo(width - padding, height - padding); - mFullPath.rLineTo(-cut, 0); - mFullPath.rLineTo(0, -cut); - mFullPath.rLineTo(cut, 0); - mFullPath.rLineTo(0, cut); - } - - if (mState == STATE_EMPTY) { - // Where the corners would be if this were a real triangle - mVirtualTop.set( - width - padding, - (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius)); - mVirtualLeft.set( - (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius), - height - padding); - - final float cutWidth = CUT_WIDTH_DP * height; - final float cutDiagInset = cutWidth * INV_TAN; - - // Cut out a smaller triangle from the center of mFullPath - mCutPath.reset(); - mCutPath.setFillType(FillType.WINDING); - mCutPath.moveTo(width - padding - cutWidth, height - padding - cutWidth); - mCutPath.lineTo(width - padding - cutWidth, mVirtualTop.y + cutDiagInset); - mCutPath.lineTo(mVirtualLeft.x + cutDiagInset, height - padding - cutWidth); - mCutPath.lineTo(width - padding - cutWidth, height - padding - cutWidth); - - // Draw empty state as only background - mForegroundPath.reset(); - mFullPath.op(mCutPath, Path.Op.DIFFERENCE); - } else if (mState == STATE_AIRPLANE) { - // Airplane mode is slashed, fully drawn background - mForegroundPath.reset(); - mSlash.draw((int) height, (int) width, canvas, mPaint); - } else if (mState != STATE_CARRIER_CHANGE) { - mForegroundPath.reset(); - int sigWidth = Math.round(calcFit(mLevel / (mNumLevels - 1)) * (width - 2 * padding)); - mForegroundPath.addRect(padding, padding, padding + sigWidth, height - padding, - Direction.CW); - mForegroundPath.op(mFullPath, Op.INTERSECT); - } - - canvas.drawPath(mFullPath, mPaint); - canvas.drawPath(mForegroundPath, mForegroundPaint); - if (mState == STATE_CUT) { - mXPath.reset(); - mXPath.moveTo(X_PATH[0][0] * width, X_PATH[0][1] * height); - for (int i = 1; i < X_PATH.length; i++) { - mXPath.rLineTo(X_PATH[i][0] * width, X_PATH[i][1] * height); - } - canvas.drawPath(mXPath, mForegroundPaint); - } - if (isRtl) { - canvas.restore(); - } - } - - private void drawDot(Path fullPath, Path foregroundPath, float x, float y, float dotSize, - int i) { - Path p = (i == mCurrentDot) ? foregroundPath : fullPath; - p.addRect(x, y, x + dotSize, y + dotSize, Direction.CW); - } - - // This is a fit line based on previous values of provided in assets, but if - // you look at the a plot of this actual fit, it makes a lot of sense, what it does - // is compress the areas that are very visually easy to see changes (the middle sections) - // and spread out the sections that are hard to see (each end of the icon). - // The current fit is cubic, but pretty easy to change the way the code is written (just add - // terms to the end of FIT). - private float calcFit(float v) { - float ret = 0; - float t = v; - for (int i = 0; i < FIT.length; i++) { - ret += FIT[i] * t; - t *= v; - } - return ret; - } - - @Override - public int getAlpha() { - return mPaint.getAlpha(); - } - - @Override - public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { - mPaint.setAlpha(alpha); - mForegroundPaint.setAlpha(alpha); - } - - @Override - public void setColorFilter(@Nullable ColorFilter colorFilter) { - mPaint.setColorFilter(colorFilter); - mForegroundPaint.setColorFilter(colorFilter); - } - - @Override - public int getOpacity() { - return 255; - } - - @Override - public boolean setVisible(boolean visible, boolean restart) { - mVisible = visible; - updateAnimation(); - return super.setVisible(visible, restart); - } - - private final Runnable mChangeDot = new Runnable() { - @Override - public void run() { - if (++mCurrentDot == NUM_DOTS) { - mCurrentDot = 0; - } - invalidateSelf(); - mHandler.postDelayed(mChangeDot, DOT_DELAY); - } - }; - - public static int getLevel(int fullState) { - return fullState & LEVEL_MASK; - } - - public static int getState(int fullState) { - return (fullState & STATE_MASK) >> STATE_SHIFT; - } - - public static int getNumLevels(int fullState) { - return (fullState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT; - } - - public static int getState(int level, int numLevels, boolean cutOut) { - return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT) - | (numLevels << NUM_LEVEL_SHIFT) - | level; - } - - public static int getCarrierChangeState(int numLevels) { - return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT); - } - - public static int getEmptyState(int numLevels) { - return (STATE_EMPTY << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT); - } - - public static int getAirplaneModeState(int numLevels) { - return (STATE_AIRPLANE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT); - } - - private final class SlashArtist { - // These values are derived in un-rotated (vertical) orientation - private static final float SLASH_WIDTH = 1.8384776f; - private static final float SLASH_HEIGHT = 22f; - private static final float CENTER_X = 10.65f; - private static final float CENTER_Y = 15.869239f; - private static final float SCALE = 24f; - - // Bottom is derived during animation - private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE; - private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE; - private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE; - private static final float BOTTOM = (CENTER_Y + (SLASH_HEIGHT / 2)) / SCALE; - // Draw the slash washington-monument style; rotate to no-u-turn style - private static final float ROTATION = -45f; - - private final Path mPath = new Path(); - private final RectF mSlashRect = new RectF(); - - void draw(int height, int width, @NonNull Canvas canvas, Paint paint) { - Matrix m = new Matrix(); - final float radius = scale(SlashDrawable.CORNER_RADIUS, width); - updateRect( - scale(LEFT, width), - scale(TOP, height), - scale(RIGHT, width), - scale(BOTTOM, height)); - - mPath.reset(); - // Draw the slash vertically - mPath.addRoundRect(mSlashRect, radius, radius, Direction.CW); - m.setRotate(ROTATION, width / 2, height / 2); - mPath.transform(m); - canvas.drawPath(mPath, paint); - - // Rotate back to vertical, and draw the cut-out rect next to this one - m.setRotate(-ROTATION, width / 2, height / 2); - mPath.transform(m); - m.setTranslate(mSlashRect.width(), 0); - mPath.transform(m); - mPath.addRoundRect(mSlashRect, radius, radius, Direction.CW); - m.setRotate(ROTATION, width / 2, height / 2); - mPath.transform(m); - canvas.clipOutPath(mPath); - } - - void updateRect(float left, float top, float right, float bottom) { - mSlashRect.left = left; - mSlashRect.top = top; - mSlashRect.right = right; - mSlashRect.bottom = bottom; - } - - private float scale(float frac, int width) { - return frac * width; - } - } -} 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 1bf719ae68af..24920cba21f5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -111,6 +111,7 @@ import android.view.IWindowManager; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; +import android.view.RemoteAnimationAdapter; import android.view.ThreadedRenderer; import android.view.View; import android.view.ViewGroup; @@ -140,7 +141,6 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.ActivityStarterDelegate; import com.android.systemui.AutoReinflateContainer; -import com.android.systemui.charging.WirelessChargingAnimation; import com.android.systemui.DemoMode; import com.android.systemui.Dependency; import com.android.systemui.EventLogTags; @@ -152,6 +152,7 @@ import com.android.systemui.SystemUI; import com.android.systemui.SystemUIFactory; import com.android.systemui.UiOffloadThread; import com.android.systemui.assist.AssistManager; +import com.android.systemui.charging.WirelessChargingAnimation; import com.android.systemui.classifier.FalsingLog; import com.android.systemui.classifier.FalsingManager; import com.android.systemui.colorextraction.SysuiColorExtractor; @@ -181,6 +182,7 @@ import com.android.systemui.stackdivider.WindowManagerProxy; import com.android.systemui.statusbar.ActivatableNotificationView; import com.android.systemui.statusbar.BackDropView; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.DismissView; import com.android.systemui.statusbar.DragDownHelper; import com.android.systemui.statusbar.EmptyShadeView; @@ -208,6 +210,7 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.AboveShelfObserver; import com.android.systemui.statusbar.notification.ActivityLaunchAnimator; import com.android.systemui.statusbar.notification.VisualStabilityManager; +import com.android.systemui.statusbar.phone.HeadsUpManagerPhone; import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; @@ -219,6 +222,7 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.statusbar.policy.ExtensionController; import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.policy.HeadsUpUtil; import com.android.systemui.statusbar.policy.KeyguardMonitor; import com.android.systemui.statusbar.policy.KeyguardMonitorImpl; import com.android.systemui.statusbar.policy.KeyguardUserSwitcher; @@ -809,15 +813,14 @@ public class StatusBar extends SystemUI implements DemoMode, .commit(); mIconController = Dependency.get(StatusBarIconController.class); - mHeadsUpManager = new HeadsUpManager(context, mStatusBarWindow, mGroupManager); - mHeadsUpManager.setBar(this); + mHeadsUpManager = new HeadsUpManagerPhone(context, mStatusBarWindow, mGroupManager, this, + mVisualStabilityManager); mHeadsUpManager.addListener(this); mHeadsUpManager.addListener(mNotificationPanel); mHeadsUpManager.addListener(mGroupManager); mHeadsUpManager.addListener(mVisualStabilityManager); mNotificationPanel.setHeadsUpManager(mHeadsUpManager); mGroupManager.setHeadsUpManager(mHeadsUpManager); - mHeadsUpManager.setVisualStabilityManager(mVisualStabilityManager); putComponent(HeadsUpManager.class, mHeadsUpManager); mEntryManager.setUpWithPresenter(this, mStackScroller, this, mHeadsUpManager); @@ -1348,7 +1351,8 @@ public class StatusBar extends SystemUI implements DemoMode, @Override public void onPerformRemoveNotification(StatusBarNotification n) { - if (mStackScroller.hasPulsingNotifications() && mHeadsUpManager.getAllEntries().isEmpty()) { + if (mStackScroller.hasPulsingNotifications() && + !mHeadsUpManager.hasHeadsUpNotifications()) { // We were showing a pulse for a notification, but no notifications are pulsing anymore. // Finish the pulse. mDozeScrimController.pulseOutNow(); @@ -2097,9 +2101,8 @@ public class StatusBar extends SystemUI implements DemoMode, } public void maybeEscalateHeadsUp() { - Collection<HeadsUpManager.HeadsUpEntry> entries = mHeadsUpManager.getAllEntries(); - for (HeadsUpManager.HeadsUpEntry entry : entries) { - final StatusBarNotification sbn = entry.entry.notification; + mHeadsUpManager.getAllEntries().forEach(entry -> { + final StatusBarNotification sbn = entry.notification; final Notification notification = sbn.getNotification(); if (notification.fullScreenIntent != null) { if (DEBUG) { @@ -2109,11 +2112,11 @@ public class StatusBar extends SystemUI implements DemoMode, EventLog.writeEvent(EventLogTags.SYSUI_HEADS_UP_ESCALATION, sbn.getKey()); notification.fullScreenIntent.send(); - entry.entry.notifyFullScreenIntentLaunched(); + entry.notifyFullScreenIntentLaunched(); } catch (PendingIntent.CanceledException e) { } } - } + }); mHeadsUpManager.releaseAllImmediately(); } @@ -2459,14 +2462,25 @@ public class StatusBar extends SystemUI implements DemoMode, } @Override - public void showChargingAnimation(int batteryLevel) { - if (mDozing) { - // ambient - } else if (mKeyguardManager.isKeyguardLocked()) { - // lockscreen + public void showWirelessChargingAnimation(int batteryLevel) { + if (mDozing || mKeyguardManager.isKeyguardLocked()) { + // on ambient or lockscreen, hide notification panel + WirelessChargingAnimation.makeWirelessChargingAnimation(mContext, null, + batteryLevel, new WirelessChargingAnimation.Callback() { + @Override + public void onAnimationStarting() { + CrossFadeHelper.fadeOut(mNotificationPanel, 1); + } + + @Override + public void onAnimationEnded() { + CrossFadeHelper.fadeIn(mNotificationPanel); + } + }).show(); } else { + // workspace WirelessChargingAnimation.makeWirelessChargingAnimation(mContext, null, - batteryLevel).show(); + batteryLevel, null).show(); } } @@ -2836,7 +2850,7 @@ public class StatusBar extends SystemUI implements DemoMode, Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); int result = ActivityManager.START_CANCELED; ActivityOptions options = new ActivityOptions(getActivityOptions( - null /* sourceNotification */)); + null /* remoteAnimation */)); options.setDisallowEnterPictureInPictureWhileLaunching( disallowEnterPictureInPictureWhileLaunching); if (intent == KeyguardBottomAreaView.INSECURE_CAMERA_INTENT) { @@ -3882,6 +3896,7 @@ public class StatusBar extends SystemUI implements DemoMode, private void instantCollapseNotificationPanel() { mNotificationPanel.instantCollapse(); + runPostCollapseRunnables(); } @Override @@ -4393,11 +4408,13 @@ public class StatusBar extends SystemUI implements DemoMode, @Override public void onScreenTurnedOn() { + mScrimController.onScreenTurnedOn(); } @Override public void onScreenTurnedOff() { mFalsingManager.onScreenOff(); + mScrimController.onScreenTurnedOff(); // If we pulse in from AOD, we turn the screen off first. However, updatingIsKeyguard // in that case destroys the HeadsUpManager state, so don't do it in that case. if (!isPulsing()) { @@ -4658,24 +4675,22 @@ public class StatusBar extends SystemUI implements DemoMode, @Override public void onPulseStarted() { callback.onPulseStarted(); - Collection<HeadsUpManager.HeadsUpEntry> pulsingEntries = - mHeadsUpManager.getAllEntries(); - if (!pulsingEntries.isEmpty()) { + if (mHeadsUpManager.hasHeadsUpNotifications()) { // Only pulse the stack scroller if there's actually something to show. // Otherwise just show the always-on screen. - setPulsing(pulsingEntries); + setPulsing(true); } } @Override public void onPulseFinished() { callback.onPulseFinished(); - setPulsing(null); + setPulsing(false); } - private void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing) { + private void setPulsing(boolean pulsing) { mNotificationPanel.setPulsing(pulsing); - mVisualStabilityManager.setPulsing(pulsing != null); + mVisualStabilityManager.setPulsing(pulsing); mIgnoreTouchWhilePulsing = false; } }, reason); @@ -4823,7 +4838,7 @@ public class StatusBar extends SystemUI implements DemoMode, // for heads up notifications - protected HeadsUpManager mHeadsUpManager; + protected HeadsUpManagerPhone mHeadsUpManager; private AboveShelfObserver mAboveShelfObserver; @@ -4926,7 +4941,7 @@ public class StatusBar extends SystemUI implements DemoMode, // Release the HUN notification to the shade. if (isPresenterFullyCollapsed()) { - HeadsUpManager.setIsClickedNotification(row, true); + HeadsUpUtil.setIsClickedHeadsUpNotification(row, true); } // // In most cases, when FLAG_AUTO_CANCEL is set, the notification will @@ -4987,11 +5002,15 @@ public class StatusBar extends SystemUI implements DemoMode, fillInIntent = new Intent().putExtra(Notification.EXTRA_REMOTE_INPUT_DRAFT, remoteInputText.toString()); } + RemoteAnimationAdapter adapter = mActivityLaunchAnimator.getLaunchAnimation( + row); try { + ActivityManager.getService().registerRemoteAnimationForNextActivityStart( + intent.getCreatorPackage(), adapter); launchResult = intent.sendAndReturnResult(mContext, 0, fillInIntent, null, - null, null, getActivityOptions(row)); + null, null, getActivityOptions(adapter)); mActivityLaunchAnimator.setLaunchResult(launchResult); - } catch (PendingIntent.CanceledException e) { + } catch (RemoteException | PendingIntent.CanceledException e) { // the stack trace isn't very helpful here. // Just log the exception message. Log.w(TAG, "Sending contentIntent failed: " + e); @@ -5045,6 +5064,8 @@ public class StatusBar extends SystemUI implements DemoMode, } else if (!isPresenterFullyCollapsed()) { instantCollapseNotificationPanel(); visibilityChanged(false); + } else { + runPostCollapseRunnables(); } } @@ -5149,7 +5170,8 @@ public class StatusBar extends SystemUI implements DemoMode, AsyncTask.execute(() -> { int launchResult = TaskStackBuilder.create(mContext) .addNextIntentWithParentStack(intent) - .startActivities(getActivityOptions(row), + .startActivities(getActivityOptions( + mActivityLaunchAnimator.getLaunchAnimation(row)), new UserHandle(UserHandle.getUserId(appUid))); mActivityLaunchAnimator.setLaunchResult(launchResult); if (shouldCollapse()) { @@ -5284,7 +5306,7 @@ public class StatusBar extends SystemUI implements DemoMode, } try { intent.send(null, 0, null, null, null, null, getActivityOptions( - null /* sourceNotification */)); + null /* animationAdapter */)); } catch (PendingIntent.CanceledException e) { // the stack trace isn't very helpful here. // Just log the exception message. @@ -5312,10 +5334,10 @@ public class StatusBar extends SystemUI implements DemoMode, return true; } - protected Bundle getActivityOptions(ExpandableNotificationRow sourceNotification) { + protected Bundle getActivityOptions(@Nullable RemoteAnimationAdapter animationAdapter) { ActivityOptions options; - if (sourceNotification != null) { - options = mActivityLaunchAnimator.getLaunchAnimation(sourceNotification); + if (animationAdapter != null) { + options = ActivityOptions.makeRemoteAnimation(animationAdapter); } else { options = ActivityOptions.makeBasic(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java index c30f6339f8da..948f524bb188 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java @@ -106,6 +106,7 @@ public class StatusBarWindowManager implements RemoteInputController.Callback, D mLp.gravity = Gravity.TOP; mLp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; mLp.setTitle("StatusBar"); + mLp.accessibilityTitle = mContext.getString(R.string.status_bar); mLp.packageName = mContext.getPackageName(); mStatusBarView = statusBarView; mBarHeight = barHeight; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java index 1da50ad65d62..503a1b40bb0c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java @@ -36,6 +36,7 @@ import com.android.systemui.statusbar.stack.ViewState; public class StatusIconContainer extends AlphaOptimizedLinearLayout { private static final String TAG = "StatusIconContainer"; + private static final boolean DEBUG = false; private static final int MAX_ICONS = 5; private static final int MAX_DOTS = 3; @@ -94,7 +95,7 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { int childCount = getChildCount(); // Underflow === don't show content until that index int firstUnderflowIndex = -1; - android.util.Log.d(TAG, "calculateIconTransitions: start=" + translationX); + if (DEBUG) android.util.Log.d(TAG, "calculateIconTransitions: start=" + translationX); //TODO: Dots for (int i = childCount - 1; i >= 0; i--) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java index 53dfb244c776..040d7ec32ecc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java @@ -16,119 +16,69 @@ package com.android.systemui.statusbar.policy; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; import android.database.ContentObserver; +import android.os.SystemClock; import android.os.Handler; import android.os.Looper; -import android.os.SystemClock; -import android.provider.Settings; -import android.support.v4.util.ArraySet; import android.util.ArrayMap; +import android.provider.Settings; import android.util.Log; -import android.util.Pools; -import android.view.View; -import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityEvent; import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.NotificationData; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.notification.VisualStabilityManager; -import com.android.systemui.statusbar.phone.NotificationGroupManager; -import com.android.systemui.statusbar.phone.StatusBar; import java.io.FileDescriptor; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collection; +import java.util.Iterator; +import java.util.stream.Stream; import java.util.HashMap; import java.util.HashSet; -import java.util.Stack; /** * A manager which handles heads up notifications which is a special mode where * they simply peek from the top of the screen. */ -public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener, - VisualStabilityManager.Callback { +public class HeadsUpManager { private static final String TAG = "HeadsUpManager"; private static final boolean DEBUG = false; private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; - private static final int TAG_CLICKED_NOTIFICATION = R.id.is_clicked_heads_up_tag; - private final int mHeadsUpNotificationDecay; - private final int mMinimumDisplayTime; + protected final Clock mClock = new Clock(); + protected final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>(); + protected final Handler mHandler = new Handler(Looper.getMainLooper()); - private final int mTouchAcceptanceDelay; - private final ArrayMap<String, Long> mSnoozedPackages; - private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>(); - private final int mDefaultSnoozeLengthMs; - private final Handler mHandler = new Handler(Looper.getMainLooper()); - private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() { + protected final Context mContext; - private Stack<HeadsUpEntry> mPoolObjects = new Stack<>(); + protected int mHeadsUpNotificationDecay; + protected int mMinimumDisplayTime; + protected int mTouchAcceptanceDelay; + protected int mSnoozeLengthMs; + protected boolean mHasPinnedNotification; + protected int mUser; - @Override - public HeadsUpEntry acquire() { - if (!mPoolObjects.isEmpty()) { - return mPoolObjects.pop(); - } - return new HeadsUpEntry(); - } + private final HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>(); + private final ArrayMap<String, Long> mSnoozedPackages; - @Override - public boolean release(HeadsUpEntry instance) { - instance.reset(); - mPoolObjects.push(instance); - return true; - } - }; - - private final View mStatusBarWindowView; - private final int mStatusBarHeight; - private final Context mContext; - private final NotificationGroupManager mGroupManager; - private StatusBar mBar; - private int mSnoozeLengthMs; - private ContentObserver mSettingsObserver; - private HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>(); - private HashSet<String> mSwipedOutKeys = new HashSet<>(); - private int mUser; - private Clock mClock; - private boolean mReleaseOnExpandFinish; - private boolean mTrackingHeadsUp; - private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>(); - private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed - = new ArraySet<>(); - private boolean mIsExpanded; - private boolean mHasPinnedNotification; - private int[] mTmpTwoArray = new int[2]; - private boolean mHeadsUpGoingAway; - private boolean mWaitingOnCollapseWhenGoingAway; - private boolean mIsObserving; - private boolean mRemoteInputActive; - private float mExpandedHeight; - private VisualStabilityManager mVisualStabilityManager; - private int mStatusBarState; - - public HeadsUpManager(final Context context, View statusBarWindowView, - NotificationGroupManager groupManager) { + public HeadsUpManager(@NonNull final Context context) { mContext = context; - Resources resources = mContext.getResources(); - mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay); - mSnoozedPackages = new ArrayMap<>(); - mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms); - mSnoozeLengthMs = mDefaultSnoozeLengthMs; + Resources resources = context.getResources(); mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time); mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay); - mClock = new Clock(); + mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay); + mSnoozedPackages = new ArrayMap<>(); + int defaultSnoozeLengthMs = + resources.getInteger(R.integer.heads_up_default_snooze_length_ms); mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(), - SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs); - mSettingsObserver = new ContentObserver(mHandler) { + SETTING_HEADS_UP_SNOOZE_LENGTH_MS, defaultSnoozeLengthMs); + ContentObserver settingsObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { final int packageSnoozeLengthMs = Settings.Global.getInt( @@ -141,48 +91,27 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL }; context.getContentResolver().registerContentObserver( Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false, - mSettingsObserver); - mStatusBarWindowView = statusBarWindowView; - mGroupManager = groupManager; - mStatusBarHeight = resources.getDimensionPixelSize( - com.android.internal.R.dimen.status_bar_height); - } - - private void updateTouchableRegionListener() { - boolean shouldObserve = mHasPinnedNotification || mHeadsUpGoingAway - || mWaitingOnCollapseWhenGoingAway; - if (shouldObserve == mIsObserving) { - return; - } - if (shouldObserve) { - mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this); - mStatusBarWindowView.requestLayout(); - } else { - mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this); - } - mIsObserving = shouldObserve; + settingsObserver); } - public void setBar(StatusBar bar) { - mBar = bar; - } - - public void addListener(OnHeadsUpChangedListener listener) { + /** + * Adds an OnHeadUpChangedListener to observe events. + */ + public void addListener(@NonNull OnHeadsUpChangedListener listener) { mListeners.add(listener); } - public void removeListener(OnHeadsUpChangedListener listener) { + /** + * Removes the OnHeadUpChangedListener from the observer list. + */ + public void removeListener(@NonNull OnHeadsUpChangedListener listener) { mListeners.remove(listener); } - public StatusBar getBar() { - return mBar; - } - /** * Called when posting a new notification to the heads up. */ - public void showNotification(NotificationData.Entry headsUp) { + public void showNotification(@NonNull NotificationData.Entry headsUp) { if (DEBUG) Log.v(TAG, "showNotification"); addHeadsUpEntry(headsUp); updateNotification(headsUp, true); @@ -192,7 +121,7 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL /** * Called when updating or posting a notification to the heads up. */ - public void updateNotification(NotificationData.Entry headsUp, boolean alert) { + public void updateNotification(@NonNull NotificationData.Entry headsUp, boolean alert) { if (DEBUG) Log.v(TAG, "updateNotification"); headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); @@ -204,14 +133,13 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL // with the groupmanager return; } - headsUpEntry.updateEntry(); + headsUpEntry.updateEntry(true /* updatePostTime */); setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUp)); } } - private void addHeadsUpEntry(NotificationData.Entry entry) { - HeadsUpEntry headsUpEntry = mEntryPool.acquire(); - + private void addHeadsUpEntry(@NonNull NotificationData.Entry entry) { + HeadsUpEntry headsUpEntry = createHeadsUpEntry(); // This will also add the entry to the sortedList headsUpEntry.setEntry(entry); mHeadsUpEntries.put(entry.key, headsUpEntry); @@ -223,16 +151,17 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } - private boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) { - return mStatusBarState != StatusBarState.KEYGUARD - && !mIsExpanded || hasFullScreenIntent(entry); + protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationData.Entry entry) { + return hasFullScreenIntent(entry); } - private boolean hasFullScreenIntent(NotificationData.Entry entry) { + protected boolean hasFullScreenIntent(@NonNull NotificationData.Entry entry) { return entry.notification.getNotification().fullScreenIntent != null; } - private void setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned) { + protected void setEntryPinned( + @NonNull HeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned) { + if (DEBUG) Log.v(TAG, "setEntryPinned: " + isPinned); ExpandableNotificationRow row = headsUpEntry.entry.row; if (row.isPinned() != isPinned) { row.setPinned(isPinned); @@ -247,33 +176,35 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL } } - private void removeHeadsUpEntry(NotificationData.Entry entry) { + protected void removeHeadsUpEntry(@NonNull NotificationData.Entry entry) { HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key); + onHeadsUpEntryRemoved(remove); + } + + protected void onHeadsUpEntryRemoved(@NonNull HeadsUpEntry remove) { + NotificationData.Entry entry = remove.entry; entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); entry.row.setHeadsUp(false); setEntryPinned(remove, false /* isPinned */); for (OnHeadsUpChangedListener listener : mListeners) { listener.onHeadsUpStateChanged(entry, false); } - mEntryPool.release(remove); + releaseHeadsUpEntry(remove); } - public void removeAllHeadsUpEntries() { - for (String key : mHeadsUpEntries.keySet()) { - removeHeadsUpEntry(mHeadsUpEntries.get(key).entry); - } - } - - private void updatePinnedMode() { + protected void updatePinnedMode() { boolean hasPinnedNotification = hasPinnedNotificationInternal(); if (hasPinnedNotification == mHasPinnedNotification) { return; } + if (DEBUG) { + Log.v(TAG, "Pinned mode changed: " + mHasPinnedNotification + " -> " + + hasPinnedNotification); + } mHasPinnedNotification = hasPinnedNotification; if (mHasPinnedNotification) { MetricsLogger.count(mContext, "note_peek", 1); } - updateTouchableRegionListener(); for (OnHeadsUpChangedListener listener : mListeners) { listener.onHeadsUpPinnedModeChanged(hasPinnedNotification); } @@ -285,47 +216,36 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL * @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 */ - public boolean removeNotification(String key, boolean ignoreEarliestRemovalTime) { - if (DEBUG) Log.v(TAG, "remove"); - if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) { - releaseImmediately(key); - return true; - } else { - getHeadsUpEntry(key).removeAsSoonAsPossible(); - return false; - } + public boolean removeNotification(@NonNull String key, boolean ignoreEarliestRemovalTime) { + if (DEBUG) Log.v(TAG, "removeNotification"); + releaseImmediately(key); + return true; } - private boolean wasShownLongEnough(String key) { - HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); - HeadsUpEntry topEntry = getTopEntry(); - if (mSwipedOutKeys.contains(key)) { - // We always instantly dismiss views being manually swiped out. - mSwipedOutKeys.remove(key); - return true; - } - if (headsUpEntry != topEntry) { - return true; - } - return headsUpEntry.wasShownLongEnough(); - } - - public boolean isHeadsUp(String key) { + /** + * Returns if the given notification is in the Heads Up Notification list or not. + */ + public boolean isHeadsUp(@NonNull String key) { return mHeadsUpEntries.containsKey(key); } /** - * Push any current Heads Up notification down into the shade. + * Pushes any current Heads Up notification down into the shade. */ public void releaseAllImmediately() { if (DEBUG) Log.v(TAG, "releaseAllImmediately"); - ArrayList<String> keys = new ArrayList<>(mHeadsUpEntries.keySet()); - for (String key : keys) { - releaseImmediately(key); + Iterator<HeadsUpEntry> iterator = mHeadsUpEntries.values().iterator(); + while (iterator.hasNext()) { + HeadsUpEntry entry = iterator.next(); + iterator.remove(); + onHeadsUpEntryRemoved(entry); } } - public void releaseImmediately(String key) { + /** + * Pushes the given Heads Up notification down into the shade. + */ + public void releaseImmediately(@NonNull String key) { HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); if (headsUpEntry == null) { return; @@ -334,11 +254,14 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL removeHeadsUpEntry(shadeEntry); } - public boolean isSnoozed(String packageName) { + /** + * Returns if the given notification is snoozed or not. + */ + public boolean isSnoozed(@NonNull String packageName) { final String key = snoozeKey(packageName, mUser); Long snoozedUntil = mSnoozedPackages.get(key); if (snoozedUntil != null) { - if (snoozedUntil > SystemClock.elapsedRealtime()) { + if (snoozedUntil > mClock.currentTimeMillis()) { if (DEBUG) Log.v(TAG, key + " snoozed"); return true; } @@ -347,39 +270,71 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL return false; } + /** + * Snoozes all current Heads Up Notifications. + */ public void snooze() { for (String key : mHeadsUpEntries.keySet()) { HeadsUpEntry entry = mHeadsUpEntries.get(key); String packageName = entry.entry.notification.getPackageName(); mSnoozedPackages.put(snoozeKey(packageName, mUser), - SystemClock.elapsedRealtime() + mSnoozeLengthMs); + mClock.currentTimeMillis() + mSnoozeLengthMs); } - mReleaseOnExpandFinish = true; } - private static String snoozeKey(String packageName, int user) { + @NonNull + private static String snoozeKey(@NonNull String packageName, int user) { return user + "," + packageName; } - private HeadsUpEntry getHeadsUpEntry(String key) { + @Nullable + protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) { return mHeadsUpEntries.get(key); } - public NotificationData.Entry getEntry(String key) { - return mHeadsUpEntries.get(key).entry; + /** + * Returns the entry of given Heads Up Notification. + * + * @param key Key of heads up notification + */ + @Nullable + public NotificationData.Entry getEntry(@NonNull String key) { + HeadsUpEntry entry = mHeadsUpEntries.get(key); + return entry != null ? entry.entry : null; + } + + /** + * Returns the stream of all current Heads Up Notifications. + */ + @NonNull + public Stream<NotificationData.Entry> getAllEntries() { + return mHeadsUpEntries.values().stream().map(headsUpEntry -> headsUpEntry.entry); + } + + /** + * Returns the top Heads Up Notification, which appeares to show at first. + */ + @Nullable + public NotificationData.Entry getTopEntry() { + HeadsUpEntry topEntry = getTopHeadsUpEntry(); + return (topEntry != null) ? topEntry.entry : null; } - public Collection<HeadsUpEntry> getAllEntries() { - return mHeadsUpEntries.values(); + /** + * Returns if any heads up notification is available or not. + */ + public boolean hasHeadsUpNotifications() { + return !mHeadsUpEntries.isEmpty(); } - public HeadsUpEntry getTopEntry() { + @Nullable + protected HeadsUpEntry getTopHeadsUpEntry() { if (mHeadsUpEntries.isEmpty()) { return null; } HeadsUpEntry topEntry = null; for (HeadsUpEntry entry: mHeadsUpEntries.values()) { - if (topEntry == null || entry.compareTo(topEntry) == -1) { + if (topEntry == null || entry.compareTo(topEntry) < 0) { topEntry = entry; } } @@ -387,56 +342,22 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL } /** - * Decides whether a click is invalid for a notification, i.e it has not been shown long enough - * that a user might have consciously clicked on it. - * - * @param key the key of the touched notification - * @return whether the touch is invalid and should be discarded + * Sets the current user. */ - public boolean shouldSwallowClick(String key) { - HeadsUpEntry entry = mHeadsUpEntries.get(key); - if (entry != null && mClock.currentTimeMillis() < entry.postTime) { - return true; - } - return false; - } - - public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { - if (mIsExpanded || mBar.isBouncerShowing()) { - // The touchable region is always the full area when expanded - return; - } - if (mHasPinnedNotification) { - ExpandableNotificationRow topEntry = getTopEntry().entry.row; - if (topEntry.isChildInGroup()) { - final ExpandableNotificationRow groupSummary - = mGroupManager.getGroupSummary(topEntry.getStatusBarNotification()); - if (groupSummary != null) { - topEntry = groupSummary; - } - } - topEntry.getLocationOnScreen(mTmpTwoArray); - int minX = mTmpTwoArray[0]; - int maxX = mTmpTwoArray[0] + topEntry.getWidth(); - int maxY = topEntry.getIntrinsicHeight(); - - info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - info.touchableRegion.set(minX, 0, maxX, maxY); - } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) { - info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight); - } - } - public void setUser(int user) { mUser = user; } - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { pw.println("HeadsUpManager state:"); + dumpInternal(fd, pw, args); + } + + protected void dumpInternal( + @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay); pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); - pw.print(" now="); pw.println(SystemClock.elapsedRealtime()); + pw.print(" now="); pw.println(mClock.currentTimeMillis()); pw.print(" mUser="); pw.println(mUser); for (HeadsUpEntry entry: mHeadsUpEntries.values()) { pw.print(" HeadsUpEntry="); pw.println(entry.entry); @@ -449,6 +370,9 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL } } + /** + * Returns if there are any pinned Heads Up Notifications or not. + */ public boolean hasPinnedHeadsUp() { return mHasPinnedNotification; } @@ -464,14 +388,8 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL } /** - * Notifies that a notification was swiped out and will be removed. - * - * @param key the notification key + * Unpins all pinned Heads Up Notifications. */ - public void addSwipedOutNotification(String key) { - mSwipedOutKeys.add(key); - } - public void unpinAll() { for (String key : mHeadsUpEntries.keySet()) { HeadsUpEntry entry = mHeadsUpEntries.get(key); @@ -481,60 +399,13 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL } } - public void onExpandingFinished() { - if (mReleaseOnExpandFinish) { - releaseAllImmediately(); - mReleaseOnExpandFinish = false; - } else { - for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) { - if (isHeadsUp(entry.key)) { - // Maybe the heads-up was removed already - removeHeadsUpEntry(entry); - } - } - } - mEntriesToRemoveAfterExpand.clear(); - } - - public void setTrackingHeadsUp(boolean trackingHeadsUp) { - mTrackingHeadsUp = trackingHeadsUp; - } - - public boolean isTrackingHeadsUp() { - return mTrackingHeadsUp; - } - - public void setIsExpanded(boolean isExpanded) { - if (isExpanded != mIsExpanded) { - mIsExpanded = isExpanded; - if (isExpanded) { - // make sure our state is sane - mWaitingOnCollapseWhenGoingAway = false; - mHeadsUpGoingAway = false; - updateTouchableRegionListener(); - } - } - } - /** - * @return the height of the top heads up notification when pinned. This is different from the - * intrinsic height, which also includes whether the notification is system expanded and - * is mainly used when dragging down from a heads up notification. + * Returns the value of the tracking-heads-up flag. See the doc of {@code setTrackingHeadsUp} as + * well. */ - public int getTopHeadsUpPinnedHeight() { - HeadsUpEntry topEntry = getTopEntry(); - if (topEntry == null || topEntry.entry == null) { - return 0; - } - ExpandableNotificationRow row = topEntry.entry.row; - if (row.isChildInGroup()) { - final ExpandableNotificationRow groupSummary - = mGroupManager.getGroupSummary(row.getStatusBarNotification()); - if (groupSummary != null) { - row = groupSummary; - } - } - return row.getPinnedHeadsUpHeight(); + public boolean isTrackingHeadsUp() { + // Might be implemented in subclass. + return false; } /** @@ -543,7 +414,7 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL * @return -1 if the first argument should be ranked higher than the second, 1 if the second * one should be ranked higher and 0 if they are equal. */ - public int compare(NotificationData.Entry a, NotificationData.Entry b) { + public int compare(@NonNull NotificationData.Entry a, @NonNull NotificationData.Entry b) { HeadsUpEntry aEntry = getHeadsUpEntry(a.key); HeadsUpEntry bEntry = getHeadsUpEntry(b.key); if (aEntry == null || bEntry == null) { @@ -553,147 +424,62 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL } /** - * Set that we are exiting the headsUp pinned mode, but some notifications might still be - * animating out. This is used to keep the touchable regions in a sane state. - */ - public void setHeadsUpGoingAway(boolean headsUpGoingAway) { - if (headsUpGoingAway != mHeadsUpGoingAway) { - mHeadsUpGoingAway = headsUpGoingAway; - if (!headsUpGoingAway) { - waitForStatusBarLayout(); - } - updateTouchableRegionListener(); - } - } - - /** - * We need to wait on the whole panel to collapse, before we can remove the touchable region - * listener. - */ - private void waitForStatusBarLayout() { - mWaitingOnCollapseWhenGoingAway = true; - mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, - int oldTop, int oldRight, int oldBottom) { - if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) { - mStatusBarWindowView.removeOnLayoutChangeListener(this); - mWaitingOnCollapseWhenGoingAway = false; - updateTouchableRegionListener(); - } - } - }); - } - - public static void setIsClickedNotification(View child, boolean clicked) { - child.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null); - } - - public static boolean isClickedHeadsUpNotification(View child) { - Boolean clicked = (Boolean) child.getTag(TAG_CLICKED_NOTIFICATION); - return clicked != null && clicked; - } - - public void setRemoteInputActive(NotificationData.Entry entry, boolean remoteInputActive) { - HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key); - if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) { - headsUpEntry.remoteInputActive = remoteInputActive; - if (remoteInputActive) { - headsUpEntry.removeAutoRemovalCallbacks(); - } else { - headsUpEntry.updateEntry(false /* updatePostTime */); - } - } - } - - /** * Set an entry to be expanded and therefore stick in the heads up area if it's pinned * until it's collapsed again. */ - public void setExpanded(NotificationData.Entry entry, boolean expanded) { - HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key); - if (headsUpEntry != null && headsUpEntry.expanded != expanded && entry.row.isPinned()) { - headsUpEntry.expanded = expanded; - if (expanded) { - headsUpEntry.removeAutoRemovalCallbacks(); - } else { - headsUpEntry.updateEntry(false /* updatePostTime */); - } + public void setExpanded(@NonNull NotificationData.Entry entry, boolean expanded) { + HeadsUpManager.HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key); + if (headsUpEntry != null && entry.row.isPinned()) { + headsUpEntry.expanded(expanded); } } - @Override - public void onReorderingAllowed() { - mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false); - for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) { - if (isHeadsUp(entry.key)) { - // Maybe the heads-up was removed already - removeHeadsUpEntry(entry); - } - } - mEntriesToRemoveWhenReorderingAllowed.clear(); - mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(true); + @NonNull + protected HeadsUpEntry createHeadsUpEntry() { + return new HeadsUpEntry(); } - public void setVisualStabilityManager(VisualStabilityManager visualStabilityManager) { - mVisualStabilityManager = visualStabilityManager; - } - - public void setStatusBarState(int statusBarState) { - mStatusBarState = statusBarState; + protected void releaseHeadsUpEntry(@NonNull HeadsUpEntry entry) { + entry.reset(); } /** * This represents a notification and how long it is in a heads up mode. It also manages its * lifecycle automatically when created. */ - public class HeadsUpEntry implements Comparable<HeadsUpEntry> { - public NotificationData.Entry entry; + protected class HeadsUpEntry implements Comparable<HeadsUpEntry> { + @Nullable public NotificationData.Entry entry; public long postTime; - public long earliestRemovaltime; - private Runnable mRemoveHeadsUpRunnable; public boolean remoteInputActive; + public long earliestRemovaltime; public boolean expanded; - public void setEntry(final NotificationData.Entry entry) { + @Nullable private Runnable mRemoveHeadsUpRunnable; + + public void setEntry(@Nullable final NotificationData.Entry entry) { + setEntry(entry, null); + } + + public void setEntry(@Nullable final NotificationData.Entry entry, + @Nullable Runnable removeHeadsUpRunnable) { this.entry = entry; + this.mRemoveHeadsUpRunnable = removeHeadsUpRunnable; // The actual post time will be just after the heads-up really slided in postTime = mClock.currentTimeMillis() + mTouchAcceptanceDelay; - mRemoveHeadsUpRunnable = new Runnable() { - @Override - public void run() { - if (!mVisualStabilityManager.isReorderingAllowed()) { - mEntriesToRemoveWhenReorderingAllowed.add(entry); - mVisualStabilityManager.addReorderingAllowedCallback(HeadsUpManager.this); - } else if (!mTrackingHeadsUp) { - removeHeadsUpEntry(entry); - } else { - mEntriesToRemoveAfterExpand.add(entry); - } - } - }; - updateEntry(); - } - - public void updateEntry() { - updateEntry(true); + updateEntry(true /* updatePostTime */); } public void updateEntry(boolean updatePostTime) { + if (DEBUG) Log.v(TAG, "updateEntry"); + long currentTime = mClock.currentTimeMillis(); earliestRemovaltime = currentTime + mMinimumDisplayTime; if (updatePostTime) { postTime = Math.max(postTime, currentTime); } removeAutoRemovalCallbacks(); - if (mEntriesToRemoveAfterExpand.contains(entry)) { - mEntriesToRemoveAfterExpand.remove(entry); - } - if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) { - mEntriesToRemoveWhenReorderingAllowed.remove(entry); - } + if (!isSticky()) { long finishTime = postTime + mHeadsUpNotificationDecay; long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime); @@ -707,7 +493,7 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL } @Override - public int compareTo(HeadsUpEntry o) { + public int compareTo(@NonNull HeadsUpEntry o) { boolean isPinned = entry.row.isPinned(); boolean otherPinned = o.entry.row.isPinned(); if (isPinned && !otherPinned) { @@ -734,26 +520,29 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL : -1; } - public void removeAutoRemovalCallbacks() { - mHandler.removeCallbacks(mRemoveHeadsUpRunnable); - } - - public boolean wasShownLongEnough() { - return earliestRemovaltime < mClock.currentTimeMillis(); - } - - public void removeAsSoonAsPossible() { - removeAutoRemovalCallbacks(); - mHandler.postDelayed(mRemoveHeadsUpRunnable, - earliestRemovaltime - mClock.currentTimeMillis()); + public void expanded(boolean expanded) { + this.expanded = expanded; } public void reset() { - removeAutoRemovalCallbacks(); entry = null; - mRemoveHeadsUpRunnable = null; expanded = false; remoteInputActive = false; + removeAutoRemovalCallbacks(); + mRemoveHeadsUpRunnable = null; + } + + public void removeAutoRemovalCallbacks() { + if (mRemoveHeadsUpRunnable != null) + mHandler.removeCallbacks(mRemoveHeadsUpRunnable); + } + + public void removeAsSoonAsPossible() { + if (mRemoveHeadsUpRunnable != null) { + removeAutoRemovalCallbacks(); + mHandler.postDelayed(mRemoveHeadsUpRunnable, + earliestRemovaltime - mClock.currentTimeMillis()); + } } } @@ -762,5 +551,4 @@ public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsL return SystemClock.elapsedRealtime(); } } - } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpUtil.java new file mode 100644 index 000000000000..1e3c123cfbc6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpUtil.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import android.view.View; + +import com.android.systemui.R; + +/** + * A class of utility static methods for heads up notifications. + */ +public final class HeadsUpUtil { + private static final int TAG_CLICKED_NOTIFICATION = R.id.is_clicked_heads_up_tag; + + /** + * Set the given view as clicked or not-clicked. + * @param view The view to be set the flag to. + * @param clicked True to set as clicked. False to not-clicked. + */ + public static void setIsClickedHeadsUpNotification(View view, boolean clicked) { + view.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null); + } + + /** + * Check if the given view has the flag of "clicked notification" + * @param view The view to be checked. + * @return True if the view has clicked. False othrewise. + */ + public static boolean isClickedHeadsUpNotification(View view) { + Boolean clicked = (Boolean) view.getTag(TAG_CLICKED_NOTIFICATION); + return clicked != null && clicked; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java index 077c6c38c516..98bebec8511e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java @@ -27,8 +27,12 @@ import android.media.AudioManager; import android.metrics.LogMaker; import android.os.AsyncTask; import android.os.Bundle; +import android.os.RemoteException; import android.os.SystemClock; +import android.os.VibrationEffect; +import android.os.Vibrator; import android.util.AttributeSet; +import android.util.Log; import android.util.TypedValue; import android.view.HapticFeedbackConstants; import android.view.InputDevice; @@ -45,25 +49,32 @@ import android.widget.ImageView; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.Dependency; +import com.android.systemui.OverviewProxyService; import com.android.systemui.R; import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider.ButtonInterface; +import com.android.systemui.shared.system.ActivityManagerWrapper; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; public class KeyButtonView extends ImageView implements ButtonInterface { + private static final String TAG = KeyButtonView.class.getSimpleName(); private final boolean mPlaySounds; private int mContentDescriptionRes; private long mDownTime; private int mCode; private int mTouchSlop; + private int mTouchDownX; + private int mTouchDownY; private boolean mSupportsLongpress = true; private AudioManager mAudioManager; private boolean mGestureAborted; private boolean mLongClicked; private OnClickListener mOnClickListener; private final KeyButtonRipple mRipple; + private final OverviewProxyService mOverviewProxyService; + private final Vibrator mVibrator; private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); private final Runnable mCheckLongPress = new Runnable() { @@ -110,6 +121,8 @@ public class KeyButtonView extends ImageView implements ButtonInterface { mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mRipple = new KeyButtonRipple(context, this); + mVibrator = mContext.getSystemService(Vibrator.class); + mOverviewProxyService = Dependency.get(OverviewProxyService.class); setBackground(mRipple); } @@ -189,6 +202,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface { } public boolean onTouchEvent(MotionEvent ev) { + final boolean isProxyConnected = mOverviewProxyService.getProxy() != null; final int action = ev.getAction(); int x, y; if (action == MotionEvent.ACTION_DOWN) { @@ -203,23 +217,34 @@ public class KeyButtonView extends ImageView implements ButtonInterface { mDownTime = SystemClock.uptimeMillis(); mLongClicked = false; setPressed(true); + mTouchDownX = (int) ev.getX(); + mTouchDownY = (int) ev.getY(); if (mCode != 0) { sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime); } else { // Provide the same haptic feedback that the system offers for virtual keys. performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); } - playSoundEffect(SoundEffectConstants.CLICK); + if (isProxyConnected) { + // Provide small vibration for quick step or immediate down feedback + AsyncTask.execute(() -> + mVibrator.vibrate(VibrationEffect + .get(VibrationEffect.EFFECT_TICK, false))); + } else { + playSoundEffect(SoundEffectConstants.CLICK); + } removeCallbacks(mCheckLongPress); postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout()); break; case MotionEvent.ACTION_MOVE: x = (int)ev.getX(); y = (int)ev.getY(); - setPressed(x >= -mTouchSlop - && x < getWidth() + mTouchSlop - && y >= -mTouchSlop - && y < getHeight() + mTouchSlop); + boolean exceededTouchSlopX = Math.abs(x - mTouchDownX) > mTouchSlop; + boolean exceededTouchSlopY = Math.abs(y - mTouchDownY) > mTouchSlop; + if (exceededTouchSlopX || exceededTouchSlopY) { + setPressed(false); + removeCallbacks(mCheckLongPress); + } break; case MotionEvent.ACTION_CANCEL: setPressed(false); @@ -231,13 +256,21 @@ public class KeyButtonView extends ImageView implements ButtonInterface { case MotionEvent.ACTION_UP: final boolean doIt = isPressed() && !mLongClicked; setPressed(false); - // Always send a release ourselves because it doesn't seem to be sent elsewhere - // and it feels weird to sometimes get a release haptic and other times not. - if ((SystemClock.uptimeMillis() - mDownTime) > 150 && !mLongClicked) { + if (isProxyConnected) { + if (doIt) { + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + playSoundEffect(SoundEffectConstants.CLICK); + } + } else if ((SystemClock.uptimeMillis() - mDownTime) > 150 && !mLongClicked) { + // Always send a release ourselves because it doesn't seem to be sent elsewhere + // and it feels weird to sometimes get a release haptic and other times not. performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE); } if (mCode != 0) { if (doIt) { + // If there was a pending remote recents animation, then we need to + // cancel the animation now before we handle the button itself + ActivityManagerWrapper.getInstance().cancelRecentsAnimation(); sendEvent(KeyEvent.ACTION_UP, 0); sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java index 46455eb95460..4a3a173bc4ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java @@ -36,8 +36,8 @@ import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.telephony.cdma.EriInfo; +import com.android.settingslib.graph.SignalDrawable; import com.android.systemui.R; -import com.android.systemui.statusbar.phone.SignalDrawable; import com.android.systemui.statusbar.policy.NetworkController.IconState; import com.android.systemui.statusbar.policy.NetworkController.SignalCallback; import com.android.systemui.statusbar.policy.NetworkControllerImpl.Config; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java new file mode 100644 index 000000000000..c5067a6578eb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 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.systemui.statusbar.policy; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.util.KeyValueListParser; +import android.util.Log; + +import com.android.systemui.R; + +public final class SmartReplyConstants extends ContentObserver { + + private static final String TAG = "SmartReplyConstants"; + + private static final String KEY_ENABLED = "enabled"; + private static final String KEY_MAX_SQUEEZE_REMEASURE_ATTEMPTS = + "max_squeeze_remeasure_attempts"; + + private final boolean mDefaultEnabled; + private final int mDefaultMaxSqueezeRemeasureAttempts; + + private boolean mEnabled; + private int mMaxSqueezeRemeasureAttempts; + + private final Context mContext; + private final KeyValueListParser mParser = new KeyValueListParser(','); + + public SmartReplyConstants(Handler handler, Context context) { + super(handler); + + mContext = context; + final Resources resources = mContext.getResources(); + mDefaultEnabled = resources.getBoolean( + R.bool.config_smart_replies_in_notifications_enabled); + mDefaultMaxSqueezeRemeasureAttempts = resources.getInteger( + R.integer.config_smart_replies_in_notifications_max_squeeze_remeasure_attempts); + + mContext.getContentResolver().registerContentObserver( + Settings.Global.getUriFor(Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS), + false, this); + updateConstants(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + updateConstants(); + } + + private void updateConstants() { + synchronized (SmartReplyConstants.this) { + try { + mParser.setString(Settings.Global.getString(mContext.getContentResolver(), + Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS)); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Bad smart reply constants", e); + } + mEnabled = mParser.getBoolean(KEY_ENABLED, mDefaultEnabled); + mMaxSqueezeRemeasureAttempts = mParser.getInt( + KEY_MAX_SQUEEZE_REMEASURE_ATTEMPTS, mDefaultMaxSqueezeRemeasureAttempts); + } + } + + /** Returns whether smart replies in notifications are enabled. */ + public boolean isEnabled() { + return mEnabled; + } + + /** + * Returns the maximum number of times {@link SmartReplyView#onMeasure(int, int)} will try to + * find a better (narrower) line-break for a double-line smart reply button. + */ + public int getMaxSqueezeRemeasureAttempts() { + return mMaxSqueezeRemeasureAttempts; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java index 2d829af9cda7..790135fc03ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java @@ -4,23 +4,105 @@ import android.app.PendingIntent; import android.app.RemoteInput; import android.content.Context; import android.content.Intent; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.RippleDrawable; import android.os.Bundle; +import android.text.Layout; +import android.text.TextPaint; +import android.text.method.TransformationMethod; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import android.widget.LinearLayout; +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.Dependency; import com.android.systemui.R; +import java.text.BreakIterator; +import java.util.Comparator; +import java.util.PriorityQueue; + /** View which displays smart reply buttons in notifications. */ -public class SmartReplyView extends LinearLayout { +public class SmartReplyView extends ViewGroup { private static final String TAG = "SmartReplyView"; + private static final int MEASURE_SPEC_ANY_WIDTH = + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + private static final Comparator<View> DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR = + (v1, v2) -> ((v2.getMeasuredWidth() - v2.getPaddingLeft() - v2.getPaddingRight()) + - (v1.getMeasuredWidth() - v1.getPaddingLeft() - v1.getPaddingRight())); + + private static final int SQUEEZE_FAILED = -1; + + private final SmartReplyConstants mConstants; + + /** Spacing to be applied between views. */ + private final int mSpacing; + + /** Horizontal padding of smart reply buttons if all of them use only one line of text. */ + private final int mSingleLineButtonPaddingHorizontal; + + /** Horizontal padding of smart reply buttons if at least one of them uses two lines of text. */ + private final int mDoubleLineButtonPaddingHorizontal; + + /** Increase in width of a smart reply button as a result of using two lines instead of one. */ + private final int mSingleToDoubleLineButtonWidthIncrease; + + private final BreakIterator mBreakIterator; + + private PriorityQueue<Button> mCandidateButtonQueueForSqueezing; + public SmartReplyView(Context context, AttributeSet attrs) { super(context, attrs); + mConstants = Dependency.get(SmartReplyConstants.class); + + int spacing = 0; + int singleLineButtonPaddingHorizontal = 0; + int doubleLineButtonPaddingHorizontal = 0; + + final TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.SmartReplyView, + 0, 0); + final int length = arr.getIndexCount(); + for (int i = 0; i < length; i++) { + int attr = arr.getIndex(i); + switch (attr) { + case R.styleable.SmartReplyView_spacing: + spacing = arr.getDimensionPixelSize(i, 0); + break; + case R.styleable.SmartReplyView_singleLineButtonPaddingHorizontal: + singleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0); + break; + case R.styleable.SmartReplyView_doubleLineButtonPaddingHorizontal: + doubleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0); + break; + } + } + arr.recycle(); + + mSpacing = spacing; + mSingleLineButtonPaddingHorizontal = singleLineButtonPaddingHorizontal; + mDoubleLineButtonPaddingHorizontal = doubleLineButtonPaddingHorizontal; + mSingleToDoubleLineButtonWidthIncrease = + 2 * (doubleLineButtonPaddingHorizontal - singleLineButtonPaddingHorizontal); + + mBreakIterator = BreakIterator.getLineInstance(); + reallocateCandidateButtonQueueForSqueezing(); + } + + private void reallocateCandidateButtonQueueForSqueezing() { + // Instead of clearing the priority queue, we re-allocate so that it would fit all buttons + // exactly. This avoids (1) wasting memory because PriorityQueue never shrinks and + // (2) growing in onMeasure. + // The constructor throws an IllegalArgument exception if initial capacity is less than 1. + mCandidateButtonQueueForSqueezing = new PriorityQueue<>( + Math.max(getChildCount(), 1), DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR); } public void setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent) { @@ -35,6 +117,7 @@ public class SmartReplyView extends LinearLayout { } } } + reallocateCandidateButtonQueueForSqueezing(); } public static SmartReplyView inflate(Context context, ViewGroup root) { @@ -42,7 +125,8 @@ public class SmartReplyView extends LinearLayout { LayoutInflater.from(context).inflate(R.layout.smart_reply_view, root, false); } - private static Button inflateReplyButton(Context context, ViewGroup root, CharSequence choice, + @VisibleForTesting + static Button inflateReplyButton(Context context, ViewGroup root, CharSequence choice, RemoteInput remoteInput, PendingIntent pendingIntent) { Button b = (Button) LayoutInflater.from(context).inflate( R.layout.smart_reply_button, root, false); @@ -61,4 +145,376 @@ public class SmartReplyView extends LinearLayout { }); return b; } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(mContext, attrs); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams params) { + return new LayoutParams(params.width, params.height); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int targetWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED + ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec); + + // Mark all buttons as hidden and un-squeezed. + resetButtonsLayoutParams(); + + if (!mCandidateButtonQueueForSqueezing.isEmpty()) { + Log.wtf(TAG, "Single line button queue leaked between onMeasure calls"); + mCandidateButtonQueueForSqueezing.clear(); + } + + int measuredWidth = mPaddingLeft + mPaddingRight; + int maxChildHeight = 0; + int displayedChildCount = 0; + int buttonPaddingHorizontal = mSingleLineButtonPaddingHorizontal; + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) { + continue; + } + + child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(), + buttonPaddingHorizontal, child.getPaddingBottom()); + child.measure(MEASURE_SPEC_ANY_WIDTH, heightMeasureSpec); + + final int lineCount = ((Button) child).getLineCount(); + if (lineCount < 1 || lineCount > 2) { + // If smart reply has no text, or more than two lines, then don't show it. + continue; + } + + if (lineCount == 1) { + mCandidateButtonQueueForSqueezing.add((Button) child); + } + + // Remember the current measurements in case the current button doesn't fit in. + final int originalMaxChildHeight = maxChildHeight; + final int originalMeasuredWidth = measuredWidth; + final int originalButtonPaddingHorizontal = buttonPaddingHorizontal; + + final int spacing = displayedChildCount == 0 ? 0 : mSpacing; + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + measuredWidth += spacing + childWidth; + maxChildHeight = Math.max(maxChildHeight, childHeight); + + // Do we need to increase the number of lines in smart reply buttons to two? + final boolean increaseToTwoLines = + buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal + && (lineCount == 2 || measuredWidth > targetWidth); + if (increaseToTwoLines) { + measuredWidth += (displayedChildCount + 1) * mSingleToDoubleLineButtonWidthIncrease; + buttonPaddingHorizontal = mDoubleLineButtonPaddingHorizontal; + } + + // If the last button doesn't fit into the remaining width, try squeezing preceding + // smart reply buttons. + if (measuredWidth > targetWidth) { + // Keep squeezing preceding and current smart reply buttons until they all fit. + while (measuredWidth > targetWidth + && !mCandidateButtonQueueForSqueezing.isEmpty()) { + final Button candidate = mCandidateButtonQueueForSqueezing.poll(); + final int squeezeReduction = squeezeButton(candidate, heightMeasureSpec); + if (squeezeReduction != SQUEEZE_FAILED) { + maxChildHeight = Math.max(maxChildHeight, candidate.getMeasuredHeight()); + measuredWidth -= squeezeReduction; + } + } + + // If the current button still doesn't fit after squeezing all buttons, undo the + // last squeezing round. + if (measuredWidth > targetWidth) { + measuredWidth = originalMeasuredWidth; + maxChildHeight = originalMaxChildHeight; + buttonPaddingHorizontal = originalButtonPaddingHorizontal; + + // Mark all buttons from the last squeezing round as "failed to squeeze", so + // that they're re-measured without squeezing later. + markButtonsWithPendingSqueezeStatusAs(LayoutParams.SQUEEZE_STATUS_FAILED, i); + + // The current button doesn't fit, so there's no point in measuring further + // buttons. + break; + } + + // The current button fits, so mark all squeezed buttons as "successfully squeezed" + // to prevent them from being un-squeezed in a subsequent squeezing round. + markButtonsWithPendingSqueezeStatusAs(LayoutParams.SQUEEZE_STATUS_SUCCESSFUL, i); + } + + lp.show = true; + displayedChildCount++; + } + + // We're done squeezing buttons, so we can clear the priority queue. + mCandidateButtonQueueForSqueezing.clear(); + + // Finally, we need to update corner radius and re-measure some buttons. + updateCornerRadiusAndRemeasureButtonsIfNecessary(buttonPaddingHorizontal, maxChildHeight); + + setMeasuredDimension( + resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), widthMeasureSpec), + resolveSize(Math.max(getSuggestedMinimumHeight(), + mPaddingTop + maxChildHeight + mPaddingBottom), heightMeasureSpec)); + } + + private void resetButtonsLayoutParams() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.show = false; + lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_NONE; + } + } + + private int squeezeButton(Button button, int heightMeasureSpec) { + final int estimatedOptimalTextWidth = estimateOptimalSqueezedButtonTextWidth(button); + if (estimatedOptimalTextWidth == SQUEEZE_FAILED) { + return SQUEEZE_FAILED; + } + return squeezeButtonToTextWidth(button, heightMeasureSpec, estimatedOptimalTextWidth); + } + + private int estimateOptimalSqueezedButtonTextWidth(Button button) { + // Find a line-break point in the middle of the smart reply button text. + final String rawText = button.getText().toString(); + + // The button sometimes has a transformation affecting text layout (e.g. all caps). + final TransformationMethod transformation = button.getTransformationMethod(); + final String text = transformation == null ? + rawText : transformation.getTransformation(rawText, button).toString(); + final int length = text.length(); + mBreakIterator.setText(text); + + if (mBreakIterator.preceding(length / 2) == BreakIterator.DONE) { + if (mBreakIterator.next() == BreakIterator.DONE) { + // Can't find a single possible line break in either direction. + return SQUEEZE_FAILED; + } + } + + final TextPaint paint = button.getPaint(); + final int initialPosition = mBreakIterator.current(); + final float initialLeftTextWidth = Layout.getDesiredWidth(text, 0, initialPosition, paint); + final float initialRightTextWidth = + Layout.getDesiredWidth(text, initialPosition, length, paint); + float optimalTextWidth = Math.max(initialLeftTextWidth, initialRightTextWidth); + + if (initialLeftTextWidth != initialRightTextWidth) { + // See if there's a better line-break point (leading to a more narrow button) in + // either left or right direction. + final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth; + final int maxSqueezeRemeasureAttempts = mConstants.getMaxSqueezeRemeasureAttempts(); + for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) { + final int newPosition = + moveLeft ? mBreakIterator.previous() : mBreakIterator.next(); + if (newPosition == BreakIterator.DONE) { + break; + } + + final float newLeftTextWidth = Layout.getDesiredWidth(text, 0, newPosition, paint); + final float newRightTextWidth = + Layout.getDesiredWidth(text, newPosition, length, paint); + final float newOptimalTextWidth = Math.max(newLeftTextWidth, newRightTextWidth); + if (newOptimalTextWidth < optimalTextWidth) { + optimalTextWidth = newOptimalTextWidth; + } else { + break; + } + + boolean tooFar = moveLeft + ? newLeftTextWidth <= newRightTextWidth + : newLeftTextWidth >= newRightTextWidth; + if (tooFar) { + break; + } + } + } + + return (int) Math.ceil(optimalTextWidth); + } + + private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) { + int oldWidth = button.getMeasuredWidth(); + if (button.getPaddingLeft() != mDoubleLineButtonPaddingHorizontal) { + // Correct for the fact that the button was laid out with single-line horizontal + // padding. + oldWidth += mSingleToDoubleLineButtonWidthIncrease; + } + + // Re-measure the squeezed smart reply button. + button.setPadding(mDoubleLineButtonPaddingHorizontal, button.getPaddingTop(), + mDoubleLineButtonPaddingHorizontal, button.getPaddingBottom()); + final int widthMeasureSpec = MeasureSpec.makeMeasureSpec( + 2 * mDoubleLineButtonPaddingHorizontal + textWidth, MeasureSpec.AT_MOST); + button.measure(widthMeasureSpec, heightMeasureSpec); + + final int newWidth = button.getMeasuredWidth(); + + final LayoutParams lp = (LayoutParams) button.getLayoutParams(); + if (button.getLineCount() > 2 || newWidth >= oldWidth) { + lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_FAILED; + return SQUEEZE_FAILED; + } else { + lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_PENDING; + return oldWidth - newWidth; + } + } + + private void updateCornerRadiusAndRemeasureButtonsIfNecessary( + int buttonPaddingHorizontal, int maxChildHeight) { + final float cornerRadius = ((float) maxChildHeight) / 2; + final int maxChildHeightMeasure = + MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY); + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.show) { + continue; + } + + // Update corner radius. + GradientDrawable backgroundDrawable = + (GradientDrawable) ((RippleDrawable) child.getBackground()).getDrawable(0); + backgroundDrawable.setCornerRadius(cornerRadius); + + boolean requiresNewMeasure = false; + int newWidth = child.getMeasuredWidth(); + + // Re-measure reason 1: The button needs to be un-squeezed (either because it resulted + // in more than two lines or because it was unnecessary). + if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_FAILED) { + requiresNewMeasure = true; + newWidth = Integer.MAX_VALUE; + } + + // Re-measure reason 2: The button's horizontal padding is incorrect (because it was + // measured with the wrong number of lines). + if (child.getPaddingLeft() != buttonPaddingHorizontal) { + requiresNewMeasure = true; + if (buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal) { + // Decrease padding (2->1 line). + newWidth -= mSingleToDoubleLineButtonWidthIncrease; + } else { + // Increase padding (1->2 lines). + newWidth += mSingleToDoubleLineButtonWidthIncrease; + } + child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(), + buttonPaddingHorizontal, child.getPaddingBottom()); + } + + // Re-measure reason 3: The button's height is less than the max height of all buttons + // (all should have the same height). + if (child.getMeasuredHeight() != maxChildHeight) { + requiresNewMeasure = true; + } + + if (requiresNewMeasure) { + child.measure(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST), + maxChildHeightMeasure); + } + } + } + + private void markButtonsWithPendingSqueezeStatusAs(int squeezeStatus, int maxChildIndex) { + for (int i = 0; i <= maxChildIndex; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_PENDING) { + lp.squeezeStatus = squeezeStatus; + } + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + + final int width = right - left; + int position = isRtl ? width - mPaddingRight : mPaddingLeft; + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.show) { + continue; + } + + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + final int childLeft = isRtl ? position - childWidth : position; + child.layout(childLeft, 0, childLeft + childWidth, childHeight); + + final int childWidthWithSpacing = childWidth + mSpacing; + if (isRtl) { + position -= childWidthWithSpacing; + } else { + position += childWidthWithSpacing; + } + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + return lp.show && super.drawChild(canvas, child, drawingTime); + } + + @VisibleForTesting + static class LayoutParams extends ViewGroup.LayoutParams { + + /** Button is not squeezed. */ + private static final int SQUEEZE_STATUS_NONE = 0; + + /** + * Button was successfully squeezed, but it might be un-squeezed later if the squeezing + * turns out to have been unnecessary (because there's still not enough space to add another + * button). + */ + private static final int SQUEEZE_STATUS_PENDING = 1; + + /** Button was successfully squeezed and it won't be un-squeezed. */ + private static final int SQUEEZE_STATUS_SUCCESSFUL = 2; + + /** + * Button wasn't successfully squeezed. The squeezing resulted in more than two lines of + * text or it didn't reduce the button's width at all. The button will have to be + * re-measured to use only one line of text. + */ + private static final int SQUEEZE_STATUS_FAILED = 3; + + private boolean show = false; + private int squeezeStatus = SQUEEZE_STATUS_NONE; + + private LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + private LayoutParams(int width, int height) { + super(width, height); + } + + @VisibleForTesting + boolean isShown() { + return show; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java index 424858a86e58..d7a810eca02e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/AmbientState.java @@ -64,7 +64,7 @@ public class AmbientState { private boolean mPanelTracking; private boolean mExpansionChanging; private boolean mPanelFullWidth; - private Collection<HeadsUpManager.HeadsUpEntry> mPulsing; + private boolean mPulsing; private boolean mUnlockHintRunning; private boolean mQsCustomizerShowing; private int mIntrinsicPadding; @@ -315,23 +315,18 @@ public class AmbientState { } public boolean hasPulsingNotifications() { - return mPulsing != null; + return mPulsing; } - public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> hasPulsing) { + public void setPulsing(boolean hasPulsing) { mPulsing = hasPulsing; } public boolean isPulsing(NotificationData.Entry entry) { - if (mPulsing == null) { + if (!mPulsing || mHeadsUpManager == null) { return false; } - for (HeadsUpManager.HeadsUpEntry e : mPulsing) { - if (e.entry == entry) { - return true; - } - } - return false; + return mHeadsUpManager.getAllEntries().anyMatch(e -> (e == entry)); } public boolean isPanelTracking() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java index c114a6f5a6d9..1b55a5b0325f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -92,10 +92,11 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.FakeShadowView; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.VisibilityLocationProvider; +import com.android.systemui.statusbar.phone.HeadsUpManagerPhone; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.ScrimController; -import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.policy.HeadsUpUtil; import com.android.systemui.statusbar.policy.ScrollAdapter; import android.support.v4.graphics.ColorUtils; @@ -288,7 +289,7 @@ public class NotificationStackScrollLayout extends ViewGroup private HashSet<View> mClearOverlayViewsWhenFinished = new HashSet<>(); private HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations = new HashSet<>(); - private HeadsUpManager mHeadsUpManager; + private HeadsUpManagerPhone mHeadsUpManager; private boolean mTrackingHeadsUp; private ScrimController mScrimController; private boolean mForceNoOverlappingRendering; @@ -358,7 +359,7 @@ public class NotificationStackScrollLayout extends ViewGroup } }; private PorterDuffXfermode mSrcMode = new PorterDuffXfermode(PorterDuff.Mode.SRC); - private Collection<HeadsUpManager.HeadsUpEntry> mPulsing; + private boolean mPulsing; private boolean mDrawBackgroundAsSrc; private boolean mFadingOut; private boolean mParentNotFullyVisible; @@ -690,7 +691,7 @@ public class NotificationStackScrollLayout extends ViewGroup } private void updateAlgorithmHeightAndPadding() { - if (mPulsing != null) { + if (mPulsing) { mTopPadding = mClockBottom; } else { mTopPadding = mAmbientState.isDark() ? mDarkTopPadding : mRegularTopPadding; @@ -920,6 +921,27 @@ public class NotificationStackScrollLayout extends ViewGroup } /** + * @return the height of the top heads up notification when pinned. This is different from the + * intrinsic height, which also includes whether the notification is system expanded and + * is mainly used when dragging down from a heads up notification. + */ + private int getTopHeadsUpPinnedHeight() { + NotificationData.Entry topEntry = mHeadsUpManager.getTopEntry(); + if (topEntry == null) { + return 0; + } + ExpandableNotificationRow row = topEntry.row; + if (row.isChildInGroup()) { + final ExpandableNotificationRow groupSummary + = mGroupManager.getGroupSummary(row.getStatusBarNotification()); + if (groupSummary != null) { + row = groupSummary; + } + } + return row.getPinnedHeadsUpHeight(); + } + + /** * @return the position from where the appear transition ends when expanding. * Measured in absolute height. */ @@ -930,7 +952,7 @@ public class NotificationStackScrollLayout extends ViewGroup int minNotificationsForShelf = 1; if (mTrackingHeadsUp || (mHeadsUpManager.hasPinnedHeadsUp() && !mAmbientState.isDark())) { - appearPosition = mHeadsUpManager.getTopHeadsUpPinnedHeight(); + appearPosition = getTopHeadsUpPinnedHeight(); minNotificationsForShelf = 2; } else { appearPosition = 0; @@ -1198,9 +1220,9 @@ public class NotificationStackScrollLayout extends ViewGroup if (slidingChild instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) slidingChild; if (!mIsExpanded && row.isHeadsUp() && row.isPinned() - && mHeadsUpManager.getTopEntry().entry.row != row + && mHeadsUpManager.getTopEntry().row != row && mGroupManager.getGroupSummary( - mHeadsUpManager.getTopEntry().entry.row.getStatusBarNotification()) + mHeadsUpManager.getTopEntry().row.getStatusBarNotification()) != row) { continue; } @@ -2120,7 +2142,7 @@ public class NotificationStackScrollLayout extends ViewGroup @Override public boolean hasPulsingNotifications() { - return mPulsing != null; + return mPulsing; } private void updateScrollability() { @@ -2753,7 +2775,7 @@ public class NotificationStackScrollLayout extends ViewGroup } private boolean isClickedHeadsUp(View child) { - return HeadsUpManager.isClickedHeadsUpNotification(child); + return HeadsUpUtil.isClickedHeadsUpNotification(child); } /** @@ -4258,7 +4280,7 @@ public class NotificationStackScrollLayout extends ViewGroup mAnimationFinishedRunnables.add(runnable); } - public void setHeadsUpManager(HeadsUpManager headsUpManager) { + public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) { mHeadsUpManager = headsUpManager; mAmbientState.setHeadsUpManager(headsUpManager); } @@ -4326,8 +4348,8 @@ public class NotificationStackScrollLayout extends ViewGroup return mIsExpanded; } - public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing, int clockBottom) { - if (mPulsing == null && pulsing == null) { + public void setPulsing(boolean pulsing, int clockBottom) { + if (!mPulsing && !pulsing) { return; } mPulsing = pulsing; @@ -4466,7 +4488,7 @@ public class NotificationStackScrollLayout extends ViewGroup pw.println(String.format("[%s: pulsing=%s qsCustomizerShowing=%s visibility=%s" + " alpha:%f scrollY:%d]", this.getClass().getSimpleName(), - mPulsing != null ?"T":"f", + mPulsing ? "T":"f", mAmbientState.isQsCustomizerShowing() ? "T":"f", getVisibility() == View.VISIBLE ? "visible" : getVisibility() == View.GONE ? "gone" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java index 682b8493e913..04a7bd79c6ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/ViewState.java @@ -30,7 +30,7 @@ import com.android.systemui.R; import com.android.systemui.statusbar.ExpandableView; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; -import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.policy.HeadsUpUtil; /** * A state of a view. This can be used to apply a set of view properties to a view with @@ -582,7 +582,7 @@ public class ViewState { animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - HeadsUpManager.setIsClickedNotification(child, false); + HeadsUpUtil.setIsClickedHeadsUpNotification(child, false); child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null); child.setTag(TAG_START_TRANSLATION_Y, null); child.setTag(TAG_END_TRANSLATION_Y, null); diff --git a/packages/SystemUI/src/com/android/systemui/tuner/NavBarTuner.java b/packages/SystemUI/src/com/android/systemui/tuner/NavBarTuner.java index 45abd456ea16..eb0c89b06528 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/NavBarTuner.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/NavBarTuner.java @@ -18,7 +18,7 @@ import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.KEY import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.KEY_CODE_END; import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.KEY_CODE_START; import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.KEY_IMAGE_DELIM; -import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.MENU_IME; +import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.MENU_IME_ROTATE; import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.NAVSPACE; import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.NAV_BAR_LEFT; import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.NAV_BAR_RIGHT; @@ -29,24 +29,14 @@ import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.ext import android.annotation.Nullable; import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.res.Resources; import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Paint.FontMetricsInt; -import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.Handler; -import android.support.v14.preference.PreferenceFragment; -import android.support.v7.preference.DropDownPreference; import android.support.v7.preference.ListPreference; import android.support.v7.preference.Preference; import android.support.v7.preference.Preference.OnPreferenceChangeListener; -import android.support.v7.preference.Preference.OnPreferenceClickListener; -import android.support.v7.preference.PreferenceCategory; import android.text.SpannableStringBuilder; import android.text.style.ImageSpan; import android.util.Log; @@ -56,7 +46,6 @@ import android.widget.EditText; import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.statusbar.phone.NavigationBarInflaterView; import com.android.systemui.tuner.TunerService.Tunable; import java.util.ArrayList; @@ -100,7 +89,7 @@ public class NavBarTuner extends TunerPreferenceFragment { addPreferencesFromResource(R.xml.nav_bar_tuner); bindLayout((ListPreference) findPreference(LAYOUT)); bindButton(NAV_BAR_LEFT, NAVSPACE, LEFT); - bindButton(NAV_BAR_RIGHT, MENU_IME, RIGHT); + bindButton(NAV_BAR_RIGHT, MENU_IME_ROTATE, RIGHT); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java b/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java index 5c888ac89c15..6ed07f8d2c37 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java +++ b/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java @@ -49,6 +49,8 @@ import android.util.Pair; import android.view.Window; import android.view.WindowManager; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto; import com.android.settingslib.Utils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.systemui.Dependency; @@ -81,6 +83,7 @@ public class OutputChooserDialog extends Dialog private final MediaRouterWrapper mRouter; private final MediaRouterCallback mRouterCallback; private long mLastUpdateTime; + static final boolean INCLUDE_MEDIA_ROUTES = false; private boolean mIsInCall; protected boolean isAttached; @@ -174,7 +177,7 @@ public class OutputChooserDialog extends Dialog public void onAttachedToWindow() { super.onAttachedToWindow(); - if (!mIsInCall) { + if (!mIsInCall && INCLUDE_MEDIA_ROUTES) { mRouter.addCallback(mRouteSelector, mRouterCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); } @@ -201,6 +204,7 @@ public class OutputChooserDialog extends Dialog @Override public void show() { super.show(); + Dependency.get(MetricsLogger.class).visible(MetricsProto.MetricsEvent.OUTPUT_CHOOSER); mHardwareLayout.setTranslationX(getAnimTranslation()); mHardwareLayout.setAlpha(0); mHardwareLayout.animate() @@ -214,6 +218,7 @@ public class OutputChooserDialog extends Dialog @Override public void dismiss() { + Dependency.get(MetricsLogger.class).hidden(MetricsProto.MetricsEvent.OUTPUT_CHOOSER); mHardwareLayout.setTranslationX(0); mHardwareLayout.setAlpha(1); mHardwareLayout.animate() @@ -236,11 +241,15 @@ public class OutputChooserDialog extends Dialog if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_BT) { final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; if (device.getMaxConnectionState() == BluetoothProfile.STATE_DISCONNECTED) { + Dependency.get(MetricsLogger.class).action( + MetricsProto.MetricsEvent.ACTION_OUTPUT_CHOOSER_CONNECT); mBluetoothController.connect(device); } } else if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER) { final MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.tag; if (route.isEnabled()) { + Dependency.get(MetricsLogger.class).action( + MetricsProto.MetricsEvent.ACTION_OUTPUT_CHOOSER_CONNECT); route.select(); } } @@ -251,8 +260,12 @@ public class OutputChooserDialog extends Dialog if (item == null || item.tag == null) return; if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_BT) { final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; + Dependency.get(MetricsLogger.class).action( + MetricsProto.MetricsEvent.ACTION_OUTPUT_CHOOSER_DISCONNECT); mBluetoothController.disconnect(device); } else if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER) { + Dependency.get(MetricsLogger.class).action( + MetricsProto.MetricsEvent.ACTION_OUTPUT_CHOOSER_DISCONNECT); mRouter.unselect(UNSELECT_REASON_DISCONNECTED); } } @@ -272,7 +285,7 @@ public class OutputChooserDialog extends Dialog addBluetoothDevices(items); // Add remote displays - if (!mIsInCall) { + if (!mIsInCall && INCLUDE_MEDIA_ROUTES) { addRemoteDisplayRoutes(items); } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java index efa8386802e2..0203c43d3683 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java @@ -27,16 +27,15 @@ import android.os.Handler; import android.view.WindowManager.LayoutParams; import com.android.settingslib.applications.InterestingConfigChanges; -import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.Dependency; import com.android.systemui.SystemUI; import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.PluginDependencyProvider; import com.android.systemui.plugins.VolumeDialog; import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.statusbar.policy.ExtensionController; -import com.android.systemui.statusbar.policy.ExtensionController.Extension; import com.android.systemui.tuner.TunerService; import java.io.FileDescriptor; @@ -53,8 +52,8 @@ public class VolumeDialogComponent implements VolumeComponent, TunerService.Tuna public static final String VOLUME_SILENT_DO_NOT_DISTURB = "sysui_do_not_disturb"; public static final boolean DEFAULT_VOLUME_DOWN_TO_ENTER_SILENT = false; - public static final boolean DEFAULT_VOLUME_UP_TO_EXIT_SILENT = true; - public static final boolean DEFAULT_DO_NOT_DISTURB_WHEN_SILENT = true; + public static final boolean DEFAULT_VOLUME_UP_TO_EXIT_SILENT = false; + public static final boolean DEFAULT_DO_NOT_DISTURB_WHEN_SILENT = false; private final SystemUI mSysui; private final Context mContext; diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index a131a618512f..1e8e98ca2e42 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -37,6 +37,7 @@ import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.PixelFormat; import android.graphics.drawable.ColorDrawable; import android.media.AudioManager; import android.media.AudioSystem; @@ -54,6 +55,7 @@ import android.util.Log; import android.util.Slog; import android.util.SparseBooleanArray; import android.view.ContextThemeWrapper; +import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.View.AccessibilityDelegate; @@ -66,6 +68,7 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.view.animation.DecelerateInterpolator; import android.widget.ImageButton; +import android.widget.ImageView; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; @@ -107,7 +110,9 @@ public class VolumeDialogImpl implements VolumeDialog { private ViewGroup mDialogRowsView; private ViewGroup mFooter; private ImageButton mRingerIcon; + private ImageView mZenIcon; private TextView mRingerStatus; + private TextView mRingerTitle; private final List<VolumeRow> mRows = new ArrayList<>(); private ConfigurableTexts mConfigurableTexts; private final SparseBooleanArray mDynamic = new SparseBooleanArray(); @@ -173,10 +178,17 @@ public class VolumeDialogImpl implements VolumeDialog { | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); - mWindow.setTitle(VolumeDialogImpl.class.getSimpleName()); mWindow.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); mWindow.setWindowAnimations(com.android.internal.R.style.Animation_Toast); - + final WindowManager.LayoutParams lp = mWindow.getAttributes(); + lp.format = PixelFormat.TRANSLUCENT; + lp.setTitle(VolumeDialogImpl.class.getSimpleName()); + lp.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL; + lp.windowAnimations = -1; + mWindow.setAttributes(lp); + mWindow.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + mDialog.setCanceledOnTouchOutside(true); mDialog.setContentView(R.layout.volume_dialog); mDialog.setOnShowListener(dialog -> { mDialogView.setTranslationX(mDialogView.getWidth() / 2); @@ -199,13 +211,15 @@ public class VolumeDialogImpl implements VolumeDialog { rescheduleTimeoutH(); return true; }); - VolumeUiLayout hardwareLayout = VolumeUiLayout.get(mDialogView); - hardwareLayout.setOutsideTouchListener(view -> dismiss(DISMISS_REASON_TOUCH_OUTSIDE)); + VolumeUiLayout uiLayout = VolumeUiLayout.get(mDialogView); + uiLayout.updateRotation(); mDialogRowsView = mDialog.findViewById(R.id.volume_dialog_rows); mFooter = mDialog.findViewById(R.id.footer); mRingerIcon = mFooter.findViewById(R.id.ringer_icon); mRingerStatus = mFooter.findViewById(R.id.ringer_status); + mRingerTitle = mFooter.findViewById(R.id.ringer_title); + mZenIcon = mFooter.findViewById(R.id.dnd_icon); if (mRows.isEmpty()) { addRow(AudioManager.STREAM_MUSIC, @@ -339,6 +353,7 @@ public class VolumeDialogImpl implements VolumeDialog { if (stream == STREAM_ACCESSIBILITY) { row.header.setFilters(new InputFilter[] {new InputFilter.LengthFilter(13)}); } + row.dndIcon = row.view.findViewById(R.id.dnd_icon); row.slider = row.view.findViewById(R.id.volume_row_slider); row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row)); row.anim = null; @@ -394,15 +409,12 @@ public class VolumeDialogImpl implements VolumeDialog { final boolean hasVibrator = mController.hasVibrator(); if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) { if (hasVibrator) { + mController.vibrate(); mController.setRingerMode(AudioManager.RINGER_MODE_VIBRATE, false); } else { - final boolean wasZero = ss.level == 0; - mController.setStreamVolume(AudioManager.STREAM_RING, wasZero ? 1 : 0); mController.setRingerMode(AudioManager.RINGER_MODE_SILENT, false); } } else if (mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) { - final boolean wasZero = ss.level == 0; - mController.setStreamVolume(AudioManager.STREAM_RING, wasZero ? 1 : 0); mController.setRingerMode(AudioManager.RINGER_MODE_SILENT, false); } else { mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false); @@ -479,15 +491,6 @@ public class VolumeDialogImpl implements VolumeDialog { }, 50)) .start(); - if (mAccessibilityMgr.isEnabled()) { - AccessibilityEvent event = - AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - event.setPackageName(mContext.getPackageName()); - event.setClassName(CustomDialog.class.getSuperclass().getName()); - event.getText().add(mContext.getString( - R.string.volume_dialog_accessibility_dismissed_message)); - mAccessibilityMgr.sendAccessibilityEvent(event); - } Events.writeEvent(mContext, Events.EVENT_DISMISS_DIALOG, reason); mController.notifyVisible(false); synchronized (mSafetyWarningLock) { @@ -553,6 +556,8 @@ public class VolumeDialogImpl implements VolumeDialog { if (ss == null) { return; } + + enableRingerViewsH(mState.zenMode == Global.ZEN_MODE_OFF || !mState.disallowRinger); switch (mState.ringerModeInternal) { case AudioManager.RINGER_MODE_VIBRATE: mRingerStatus.setText(R.string.volume_ringer_status_vibrate); @@ -597,6 +602,28 @@ public class VolumeDialogImpl implements VolumeDialog { } } + /** + * Toggles enable state of views in a VolumeRow (not including seekbar, outputChooser or icon) + * Hides/shows zen icon + * @param enable whether to enable volume row views and hide dnd icon + */ + private void enableVolumeRowViewsH(VolumeRow row, boolean enable) { + row.header.setEnabled(enable); + row.dndIcon.setVisibility(enable ? View.GONE : View.VISIBLE); + } + + /** + * Toggles enable state of footer/ringer views + * Hides/shows zen icon + * @param enable whether to enable ringer views and hide dnd icon + */ + private void enableRingerViewsH(boolean enable) { + mRingerTitle.setEnabled(enable); + mRingerStatus.setEnabled(enable); + mRingerIcon.setEnabled(enable); + mZenIcon.setVisibility(enable ? View.GONE : View.VISIBLE); + } + private void trimObsoleteH() { if (D.BUG) Log.d(TAG, "trimObsoleteH"); for (int i = mRows.size() - 1; i >= 0; i--) { @@ -634,6 +661,8 @@ public class VolumeDialogImpl implements VolumeDialog { updateVolumeRowH(row); } updateRingerH(); + mWindow.setTitle(mContext.getString(R.string.volume_dialog_title, + getStreamLabelH(getActiveRow().ss))); } private void updateVolumeRowH(VolumeRow row) { @@ -742,6 +771,7 @@ public class VolumeDialogImpl implements VolumeDialog { if (zenMuted) { row.tracking = false; } + enableVolumeRowViewsH(row, !zenMuted); // update slider final boolean enableSlider = !zenMuted; @@ -1019,20 +1049,10 @@ public class VolumeDialogImpl implements VolumeDialog { } @Override - public boolean dispatchPopulateAccessibilityEvent(@NonNull AccessibilityEvent event) { - event.setClassName(getClass().getSuperclass().getName()); - event.setPackageName(mContext.getPackageName()); - - ViewGroup.LayoutParams params = getWindow().getAttributes(); - boolean isFullScreen = (params.width == ViewGroup.LayoutParams.MATCH_PARENT) && - (params.height == ViewGroup.LayoutParams.MATCH_PARENT); - event.setFullScreen(isFullScreen); - - if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { - if (mShowing) { - event.getText().add(mContext.getString( - R.string.volume_dialog_accessibility_shown_message, - getStreamLabelH(getActiveRow().ss))); + public boolean onTouchEvent(MotionEvent event) { + if (isShowing()) { + if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { + dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE); return true; } } @@ -1166,5 +1186,6 @@ public class VolumeDialogImpl implements VolumeDialog { private int lastAudibleLevel = 1; private View outputChooser; private TextView connectedDevice; + private ImageView dndIcon; } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeUiLayout.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeUiLayout.java index 3d4438148c39..0a3a2ab7c61f 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeUiLayout.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeUiLayout.java @@ -37,10 +37,6 @@ import com.android.systemui.util.leak.RotationUtils; public class VolumeUiLayout extends FrameLayout { private View mChild; - private int mOldHeight; - private boolean mAnimating; - private AnimatorSet mAnimation; - private boolean mHasOutsideTouch; private int mRotation = ROTATION_NONE; @Nullable private DisplayCutout mDisplayCutout; @@ -52,13 +48,11 @@ public class VolumeUiLayout extends FrameLayout { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - getViewTreeObserver().removeOnComputeInternalInsetsListener(mInsetsListener); mDisplayCutout = null; } @@ -68,16 +62,11 @@ public class VolumeUiLayout extends FrameLayout { if (mChild == null) { if (getChildCount() != 0) { mChild = getChildAt(0); - mOldHeight = mChild.getMeasuredHeight(); updateRotation(); } else { return; } } - int newHeight = mChild.getMeasuredHeight(); - if (newHeight != mOldHeight) { - animateChild(mOldHeight, newHeight); - } } @Override @@ -95,8 +84,13 @@ public class VolumeUiLayout extends FrameLayout { } } - private void updateRotation() { + public void updateRotation() { setDisplayCutout(); + if (mChild == null) { + if (getChildCount() != 0) { + mChild = getChildAt(0); + } + } int rotation = RotationUtils.getRotation(getContext()); if (rotation != mRotation) { updateSafeInsets(rotation); @@ -144,43 +138,11 @@ public class VolumeUiLayout extends FrameLayout { return r.bottom - r.top; } - - private void animateChild(int oldHeight, int newHeight) { - if (true) return; - if (mAnimating) { - mAnimation.cancel(); - } - mAnimating = true; - mAnimation = new AnimatorSet(); - mAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mAnimating = false; - } - }); - int fromTop = mChild.getTop(); - int fromBottom = mChild.getBottom(); - int toTop = fromTop - ((newHeight - oldHeight) / 2); - int toBottom = fromBottom + ((newHeight - oldHeight) / 2); - ObjectAnimator top = ObjectAnimator.ofInt(mChild, "top", fromTop, toTop); - mAnimation.playTogether(top, - ObjectAnimator.ofInt(mChild, "bottom", fromBottom, toBottom)); - } - - @Override public ViewOutlineProvider getOutlineProvider() { return super.getOutlineProvider(); } - public void setOutsideTouchListener(OnClickListener onClickListener) { - mHasOutsideTouch = true; - requestLayout(); - setOnClickListener(onClickListener); - setClickable(true); - setFocusable(true); - } - public static VolumeUiLayout get(View v) { if (v instanceof VolumeUiLayout) return (VolumeUiLayout) v; if (v.getParent() instanceof View) { @@ -188,16 +150,4 @@ public class VolumeUiLayout extends FrameLayout { } return null; } - - private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener = inoutInfo -> { - if (mHasOutsideTouch || (mChild == null)) { - inoutInfo.setTouchableInsets( - ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); - return; - } - inoutInfo.setTouchableInsets( - ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT); - inoutInfo.contentInsets.set(mChild.getLeft(), mChild.getTop(), - 0, getBottom() - mChild.getBottom()); - }; } |