diff options
50 files changed, 1539 insertions, 408 deletions
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 4f6c0c9d8f91..abe5dc347963 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -7443,7 +7443,7 @@ public class Activity extends ContextThemeWrapper final int offsetX = (anchorBounds != null) ? anchorBounds.left - actualAnchorBounds.left : 0; int offsetY = (anchorBounds != null) - ? anchorBounds.top - actualAnchorBounds.top : 0; + ? anchorBounds.bottom - actualAnchorBounds.bottom : 0; final boolean wasShowing; diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 87fcc0ad0fd3..928ef7e3863d 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -5094,24 +5094,26 @@ public final class ActivityThread { // caused by other sources, such as overlays. That means we want to be as conservative // about code changes as possible. Take the diff of the old ApplicationInfo and the new // to see if anything needs to change. + LoadedApk apk; + LoadedApk resApk; + // Update all affected loaded packages with new package information synchronized (mResourcesManager) { - // Update all affected loaded packages with new package information WeakReference<LoadedApk> ref = mPackages.get(ai.packageName); - LoadedApk apk = ref != null ? ref.get() : null; - if (apk != null) { - final ArrayList<String> oldPaths = new ArrayList<>(); - LoadedApk.makePaths(this, apk.getApplicationInfo(), oldPaths); - apk.updateApplicationInfo(ai, oldPaths); - } - - ref = mResourcePackages.get(ai.packageName); apk = ref != null ? ref.get() : null; - if (apk != null) { - final ArrayList<String> oldPaths = new ArrayList<>(); - LoadedApk.makePaths(this, apk.getApplicationInfo(), oldPaths); - apk.updateApplicationInfo(ai, oldPaths); - } - + ref = mResourcePackages.get(ai.packageName); + resApk = ref != null ? ref.get() : null; + } + if (apk != null) { + final ArrayList<String> oldPaths = new ArrayList<>(); + LoadedApk.makePaths(this, apk.getApplicationInfo(), oldPaths); + apk.updateApplicationInfo(ai, oldPaths); + } + if (resApk != null) { + final ArrayList<String> oldPaths = new ArrayList<>(); + LoadedApk.makePaths(this, resApk.getApplicationInfo(), oldPaths); + resApk.updateApplicationInfo(ai, oldPaths); + } + synchronized (mResourcesManager) { // Update all affected Resources objects to use new ResourcesImpl mResourcesManager.applyNewResourceDirsLocked(ai.sourceDir, ai.resourceDirs); } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 2e56bcf8a3de..0041879453a8 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -2675,6 +2675,7 @@ public class Notification implements Parcelable private int mActionBarColor = COLOR_INVALID; private int mBackgroundColor = COLOR_INVALID; private int mForegroundColor = COLOR_INVALID; + private int mBackgroundColorHint = COLOR_INVALID; /** * Constructs a new Builder with the defaults: @@ -3839,6 +3840,13 @@ public class Notification implements Parcelable backgroundColor); mSecondaryTextColor = NotificationColorUtil.resolveSecondaryColor(mContext, backgroundColor); + if (backgroundColor != COLOR_DEFAULT + && (mBackgroundColorHint != COLOR_INVALID || isColorized())) { + mPrimaryTextColor = NotificationColorUtil.findAlphaToMeetContrast( + mPrimaryTextColor, backgroundColor, 4.5); + mSecondaryTextColor = NotificationColorUtil.findAlphaToMeetContrast( + mSecondaryTextColor, backgroundColor, 4.5); + } } else { double backLum = NotificationColorUtil.calculateLuminance(backgroundColor); double textLum = NotificationColorUtil.calculateLuminance(mForegroundColor); @@ -4662,10 +4670,26 @@ public class Notification implements Parcelable if (mCachedContrastColorIsFor == mN.color && mCachedContrastColor != COLOR_INVALID) { return mCachedContrastColor; } - final int contrasted = NotificationColorUtil.resolveContrastColor(mContext, mN.color); + int color; + int background = mBackgroundColorHint; + if (mBackgroundColorHint == COLOR_INVALID) { + background = mContext.getColor( + com.android.internal.R.color.notification_material_background_color); + } + if (mN.color == COLOR_DEFAULT) { + ensureColors(); + color = mSecondaryTextColor; + } else { + color = NotificationColorUtil.resolveContrastColor(mContext, mN.color, + background); + } + if (Color.alpha(color) < 255) { + // alpha doesn't go well for color filters, so let's blend it manually + color = NotificationColorUtil.compositeColors(color, background); + } mCachedContrastColorIsFor = mN.color; - return mCachedContrastColor = contrasted; + return mCachedContrastColor = color; } int resolveAmbientColor() { @@ -4882,7 +4906,8 @@ public class Notification implements Parcelable if (isColorized()) { return mBackgroundColor != COLOR_INVALID ? mBackgroundColor : mN.color; } else { - return COLOR_DEFAULT; + return mBackgroundColorHint != COLOR_INVALID ? mBackgroundColorHint + : COLOR_DEFAULT; } } @@ -4913,6 +4938,17 @@ public class Notification implements Parcelable mTextColorsAreForBackground = COLOR_INVALID; ensureColors(); } + + /** + * Sets the background color for this notification to be a different one then the default. + * This is mainly used to calculate contrast and won't necessarily be applied to the + * background. + * + * @hide + */ + public void setBackgroundColorHint(int backgroundColor) { + mBackgroundColorHint = backgroundColor; + } } /** diff --git a/core/java/android/app/VrManager.java b/core/java/android/app/VrManager.java index 040b330c2f30..8014ecafa9a2 100644 --- a/core/java/android/app/VrManager.java +++ b/core/java/android/app/VrManager.java @@ -6,8 +6,6 @@ import android.content.ComponentName; import android.os.RemoteException; import android.service.vr.IVrManager; -import java.io.FileDescriptor; - /** * Used to control aspects of a devices Virtual Reality (VR) capabilities. * <p> @@ -63,32 +61,4 @@ public class VrManager { e.rethrowFromSystemServer(); } } - - /** - * Initiate connection for system controller data. - * - * @param fd Controller data file descriptor. - * - * {@hide} - */ - public void connectController(FileDescriptor fd) { - try { - mService.connectController(fd); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } - } - - /** - * Sever connection for system controller data. - * - * {@hide} - */ - public void disconnectController() { - try { - mService.disconnectController(); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } - } } diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index 211d54d1673f..63eedf539bfe 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -107,7 +107,8 @@ public abstract class CameraDevice implements AutoCloseable { /** * Create a request suitable for zero shutter lag still capture. This means * means maximizing image quality without compromising preview frame rate. - * AE/AWB/AF should be on auto mode. + * AE/AWB/AF should be on auto mode. This is intended for application-operated ZSL. For + * device-operated ZSL, use {@link CaptureRequest#CONTROL_ENABLE_ZSL} if available. * This template is guaranteed to be supported on camera devices that support the * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING PRIVATE_REPROCESSING} * capability or the @@ -115,6 +116,7 @@ public abstract class CameraDevice implements AutoCloseable { * capability. * * @see #createCaptureRequest + * @see CaptureRequest#CONTROL_ENABLE_ZSL */ public static final int TEMPLATE_ZERO_SHUTTER_LAG = 5; diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 279d73d215e3..c41fc0207d92 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -1669,6 +1669,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <code>false</code> if present.</p> * <p>For applications targeting SDK versions older than O, the value of enableZsl in all * capture templates is always <code>false</code> if present.</p> + * <p>For application-operated ZSL, use CAMERA3_TEMPLATE_ZERO_SHUTTER_LAG template.</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * * @see CaptureRequest#CONTROL_CAPTURE_INTENT diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index aedfc4b702a7..6d80c20a84af 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -2174,6 +2174,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <code>false</code> if present.</p> * <p>For applications targeting SDK versions older than O, the value of enableZsl in all * capture templates is always <code>false</code> if present.</p> + * <p>For application-operated ZSL, use CAMERA3_TEMPLATE_ZERO_SHUTTER_LAG template.</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * * @see CaptureRequest#CONTROL_CAPTURE_INTENT diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 7947620d3a94..d61fb979900b 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -42,8 +42,8 @@ import javax.crypto.Cipher; import javax.crypto.Mac; import static android.Manifest.permission.INTERACT_ACROSS_USERS; -import static android.Manifest.permission.USE_FINGERPRINT; import static android.Manifest.permission.MANAGE_FINGERPRINT; +import static android.Manifest.permission.USE_FINGERPRINT; /** * A class that coordinates access to the fingerprint hardware. @@ -910,6 +910,7 @@ public class FingerprintManager { } else if (mAuthenticationCallback != null) { mAuthenticationCallback.onAuthenticationError(clientErrMsgId, getErrorString(errMsgId, vendorCode)); + mAuthenticationCallback = null; } else if (mRemovalCallback != null) { mRemovalCallback.onRemovalError(mRemovalFingerprint, clientErrMsgId, getErrorString(errMsgId, vendorCode)); @@ -930,12 +931,14 @@ public class FingerprintManager { final AuthenticationResult result = new AuthenticationResult(mCryptoObject, fp, userId); mAuthenticationCallback.onAuthenticationSucceeded(result); + mAuthenticationCallback = null; } } private void sendAuthenticatedFailed() { if (mAuthenticationCallback != null) { - mAuthenticationCallback.onAuthenticationFailed(); + mAuthenticationCallback.onAuthenticationFailed(); + mAuthenticationCallback = null; } } diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 235f24cd3602..37c153fe6314 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -183,8 +183,10 @@ public abstract class BatteryStats implements Parcelable { * - Wakelock data (wl) gets current and max times. * New in version 20: * - Background timers and counters for: Sensor, BluetoothScan, WifiScan, Jobs, Syncs. + * New in version 21: + * - Actual (not just apportioned) Wakelock time is also recorded. */ - static final String CHECKIN_VERSION = "20"; + static final String CHECKIN_VERSION = "21"; /** * Old version, we hit 9 and ran out of room, need to remove. @@ -194,7 +196,7 @@ public abstract class BatteryStats implements Parcelable { private static final long BYTES_PER_KB = 1024; private static final long BYTES_PER_MB = 1048576; // 1024^2 private static final long BYTES_PER_GB = 1073741824; //1024^3 - + private static final String VERSION_DATA = "vers"; private static final String UID_DATA = "uid"; private static final String WAKEUP_ALARM_DATA = "wua"; @@ -205,6 +207,12 @@ public abstract class BatteryStats implements Parcelable { private static final String VIBRATOR_DATA = "vib"; private static final String FOREGROUND_DATA = "fg"; private static final String STATE_TIME_DATA = "st"; + // wl line is: + // BATTERY_STATS_CHECKIN_VERSION, uid, which, "wl", name, + // full totalTime, 'f', count, current duration, max duration, total duration, + // partial totalTime, 'p', count, current duration, max duration, total duration, + // window totalTime, 'w', count, current duration, max duration, total duration + // [Currently, full and window wakelocks have durations current = max = total = -1] private static final String WAKELOCK_DATA = "wl"; private static final String SYNC_DATA = "sy"; private static final String JOB_DATA = "jb"; @@ -2659,6 +2667,12 @@ public abstract class BatteryStats implements Parcelable { sb.append(" max="); sb.append(maxDurationMs); } + // Put actual time if it is available and different from totalTimeMillis. + final long totalDurMs = timer.getTotalDurationMsLocked(elapsedRealtimeUs/1000); + if (totalDurMs > totalTimeMillis) { + sb.append(" actual="); + sb.append(totalDurMs); + } if (timer.isRunningLocked()) { final long currentMs = timer.getCurrentDurationMsLocked(elapsedRealtimeUs/1000); if (currentMs >= 0) { @@ -2742,13 +2756,15 @@ public abstract class BatteryStats implements Parcelable { long elapsedRealtimeUs, String name, int which, String linePrefix) { long totalTimeMicros = 0; int count = 0; - long max = -1; - long current = -1; + long max = 0; + long current = 0; + long totalDuration = 0; if (timer != null) { totalTimeMicros = timer.getTotalTimeLocked(elapsedRealtimeUs, which); - count = timer.getCountLocked(which); + count = timer.getCountLocked(which); current = timer.getCurrentDurationMsLocked(elapsedRealtimeUs/1000); max = timer.getMaxDurationMsLocked(elapsedRealtimeUs/1000); + totalDuration = timer.getTotalDurationMsLocked(elapsedRealtimeUs/1000); } sb.append(linePrefix); sb.append((totalTimeMicros + 500) / 1000); // microseconds to milliseconds with rounding @@ -2759,9 +2775,16 @@ public abstract class BatteryStats implements Parcelable { sb.append(current); sb.append(','); sb.append(max); + // Partial, full, and window wakelocks are pooled, so totalDuration is meaningful (albeit + // not always tracked). Kernel wakelocks (which have name == null) have no notion of + // totalDuration independent of totalTimeMicros (since they are not pooled). + if (name != null) { + sb.append(','); + sb.append(totalDuration); + } return ","; } - + private static final void dumpLineHeader(PrintWriter pw, int uid, String category, String type) { pw.print(BATTERY_STATS_CHECKIN_VERSION); diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index eceaa31b9cf8..6aa601a98f3b 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -189,10 +189,11 @@ public abstract class VibrationEffect implements Parcelable { if (mAmplitude < -1 || mAmplitude == 0 || mAmplitude > 255) { throw new IllegalArgumentException( "amplitude must either be DEFAULT_AMPLITUDE, " + - "or between 1 and 255 inclusive"); + "or between 1 and 255 inclusive (amplitude=" + mAmplitude + ")"); } if (mTiming <= 0) { - throw new IllegalArgumentException("timing must be positive"); + throw new IllegalArgumentException( + "timing must be positive (timing=" + mTiming + ")"); } } @@ -274,24 +275,31 @@ public abstract class VibrationEffect implements Parcelable { public void validate() { if (mTimings.length != mAmplitudes.length) { throw new IllegalArgumentException( - "timing and amplitude arrays must be of equal length"); + "timing and amplitude arrays must be of equal length" + + " (timings.length=" + mTimings.length + + ", amplitudes.length=" + mAmplitudes.length + ")"); } if (!hasNonZeroEntry(mTimings)) { - throw new IllegalArgumentException("at least one timing must be non-zero"); + throw new IllegalArgumentException("at least one timing must be non-zero" + + " (timings=" + Arrays.toString(mTimings) + ")"); } for (long timing : mTimings) { if (timing < 0) { - throw new IllegalArgumentException("timings must all be >= 0"); + throw new IllegalArgumentException("timings must all be >= 0" + + " (timings=" + Arrays.toString(mTimings) + ")"); } } for (int amplitude : mAmplitudes) { if (amplitude < -1 || amplitude > 255) { throw new IllegalArgumentException( - "amplitudes must all be DEFAULT_AMPLITUDE or between 0 and 255"); + "amplitudes must all be DEFAULT_AMPLITUDE or between 0 and 255" + + " (amplitudes=" + Arrays.toString(mAmplitudes) + ")"); } } if (mRepeat < -1 || mRepeat >= mTimings.length) { - throw new IllegalArgumentException("repeat index must be >= -1"); + throw new IllegalArgumentException( + "repeat index must be within the bounds of the timings array" + + " (timings.length=" + mTimings.length + ", index=" + mRepeat +")"); } } @@ -375,7 +383,8 @@ public abstract class VibrationEffect implements Parcelable { @Override public void validate() { if (mEffectId != EFFECT_CLICK) { - throw new IllegalArgumentException("Unknown prebaked effect type"); + throw new IllegalArgumentException( + "Unknown prebaked effect type (value=" + mEffectId + ")"); } } diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index 1e55c78845d1..2f0eecae2c96 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -157,6 +157,8 @@ public abstract class Vibrator { // This call needs to continue throwing ArrayIndexOutOfBoundsException but ignore all other // exceptions for compatibility purposes if (repeat < -1 || repeat >= pattern.length) { + Log.e(TAG, "vibrate called with repeat index out of bounds" + + " (pattern.length=" + pattern.length + ", index=" + repeat + ")"); throw new ArrayIndexOutOfBoundsException(); } diff --git a/core/java/android/service/autofill/SaveInfo.java b/core/java/android/service/autofill/SaveInfo.java index 277c6223985b..9487760780e3 100644 --- a/core/java/android/service/autofill/SaveInfo.java +++ b/core/java/android/service/autofill/SaveInfo.java @@ -174,6 +174,7 @@ public final class SaveInfo implements Parcelable { SAVE_DATA_TYPE_PASSWORD, SAVE_DATA_TYPE_ADDRESS, SAVE_DATA_TYPE_CREDIT_CARD, + SAVE_DATA_TYPE_USERNAME, SAVE_DATA_TYPE_EMAIL_ADDRESS}) @Retention(RetentionPolicy.SOURCE) @interface SaveDataType{} diff --git a/core/java/android/service/vr/IVrManager.aidl b/core/java/android/service/vr/IVrManager.aidl index fc8afe9131b1..9b37a654ab98 100644 --- a/core/java/android/service/vr/IVrManager.aidl +++ b/core/java/android/service/vr/IVrManager.aidl @@ -86,17 +86,5 @@ interface IVrManager { * currently, else return the display id of the virtual display */ int getVr2dDisplayId(); - - /** - * Initiate connection for system controller data. - * - * @param fd Controller data file descriptor. - */ - void connectController(in FileDescriptor fd); - - /** - * Sever connection for system controller data. - */ - void disconnectController(); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index c250ca03565e..7399f771859a 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -6844,7 +6844,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (isAutofillable() && isAttachedToWindow()) { AutofillManager afm = getAutofillManager(); if (afm != null) { - if (enter && hasWindowFocus() && isFocused()) { + if (enter && hasWindowFocus() && isFocused() && isVisibleToUser()) { afm.notifyViewEntered(this); } else if (!hasWindowFocus() || !isFocused()) { afm.notifyViewExited(this); @@ -20430,9 +20430,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @Nullable private Drawable getAutofilledDrawable() { // Lazily load the isAutofilled drawable. if (mAttachInfo.mAutofilledDrawable == null) { - TypedArray a = mContext.getTheme().obtainStyledAttributes(AUTOFILL_HIGHLIGHT_ATTR); + Context rootContext = getRootView().getContext(); + TypedArray a = rootContext.getTheme().obtainStyledAttributes(AUTOFILL_HIGHLIGHT_ATTR); int attributeResourceId = a.getResourceId(0, 0); - mAttachInfo.mAutofilledDrawable = mContext.getDrawable(attributeResourceId); + mAttachInfo.mAutofilledDrawable = rootContext.getDrawable(attributeResourceId); a.recycle(); } diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 52c82a71fb7e..ecb25fead18c 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -98,7 +98,7 @@ import java.util.Map; * invoke the Browser application with a URL Intent rather than show it * with a WebView. For example: * <pre> - * Uri uri = Uri.parse("http://www.example.com"); + * Uri uri = Uri.parse("https://www.example.com"); * Intent intent = new Intent(Intent.ACTION_VIEW, uri); * startActivity(intent); * </pre> @@ -116,7 +116,7 @@ import java.util.Map; * <pre> * // Simplest usage: note that an exception will NOT be thrown * // if there is an error loading this page (see below). - * webview.loadUrl("http://slashdot.org/"); + * webview.loadUrl("https://example.com/"); * * // OR, you can also load from an HTML string: * String summary = "<html><body>You scored <b>192</b> points.</body></html>"; @@ -175,7 +175,7 @@ import java.util.Map; * } * }); * - * webview.loadUrl("http://developer.android.com/"); + * webview.loadUrl("https://developer.android.com/"); * </pre> * * <h3>Zoom</h3> @@ -2705,7 +2705,7 @@ public class WebView extends AbsoluteLayout * <p>Example2: an IFRAME tag. * * <pre class="prettyprint"> - * <iframe src="http://example.com/login"/> + * <iframe src="https://example.com/login"/> * </pre> * * <p>Would map to: @@ -2714,7 +2714,7 @@ public class WebView extends AbsoluteLayout * int index = structure.addChildCount(1); * ViewStructure iframe = structure.newChildFor(index); * iframe.setHtmlInfo(child.newHtmlInfoBuilder("iframe") - * .addAttribute("url", "http://example.com/login") + * .addAttribute("url", "https://example.com/login") * .build()); * </pre> */ diff --git a/core/java/com/android/internal/util/NotificationColorUtil.java b/core/java/com/android/internal/util/NotificationColorUtil.java index 2c97f8bd5971..cd41f9e9f902 100644 --- a/core/java/com/android/internal/util/NotificationColorUtil.java +++ b/core/java/com/android/internal/util/NotificationColorUtil.java @@ -286,6 +286,38 @@ public class NotificationColorUtil { } /** + * Finds a suitable alpha such that there's enough contrast. + * + * @param color the color to start searching from. + * @param backgroundColor the color to ensure contrast against. + * @param minRatio the minimum contrast ratio required. + * @return the same color as {@param color} with potentially modified alpha to meet contrast + */ + public static int findAlphaToMeetContrast(int color, int backgroundColor, double minRatio) { + int fg = color; + int bg = backgroundColor; + if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { + return color; + } + int startAlpha = Color.alpha(color); + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + + int low = startAlpha, high = 255; + for (int i = 0; i < 15 && high - low > 0; i++) { + final int alpha = (low + high) / 2; + fg = Color.argb(alpha, r, g, b); + if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { + high = alpha; + } else { + low = alpha; + } + } + return Color.argb(high, r, g, b); + } + + /** * Finds a suitable color such that there's enough contrast. * * @param color the color to start searching from. @@ -373,19 +405,19 @@ public class NotificationColorUtil { * color for the Notification's action and header text. * * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT} + * @param backgroundColor the background color to ensure the contrast against. * @return a color of the same hue with enough contrast against the backgrounds. */ - public static int resolveContrastColor(Context context, int notificationColor) { + public static int resolveContrastColor(Context context, int notificationColor, + int backgroundColor) { final int resolvedColor = resolveColor(context, notificationColor); final int actionBg = context.getColor( com.android.internal.R.color.notification_action_list); - final int notiBg = context.getColor( - com.android.internal.R.color.notification_material_background_color); int color = resolvedColor; color = NotificationColorUtil.ensureLargeTextContrast(color, actionBg); - color = NotificationColorUtil.ensureTextContrast(color, notiBg); + color = NotificationColorUtil.ensureTextContrast(color, backgroundColor); if (color != resolvedColor) { if (DEBUG){ @@ -394,7 +426,7 @@ public class NotificationColorUtil { + " and %s (over background) by changing #%s to %s", context.getPackageName(), NotificationColorUtil.contrastChange(resolvedColor, color, actionBg), - NotificationColorUtil.contrastChange(resolvedColor, color, notiBg), + NotificationColorUtil.contrastChange(resolvedColor, color, backgroundColor), Integer.toHexString(resolvedColor), Integer.toHexString(color))); } } @@ -502,6 +534,13 @@ public class NotificationColorUtil { } /** + * Composite two potentially translucent colors over each other and returns the result. + */ + public static int compositeColors(int foreground, int background) { + return ColorUtilsFromCompat.compositeColors(foreground, background); + } + + /** * Framework copy of functions needed from android.support.v4.graphics.ColorUtils. */ private static class ColorUtilsFromCompat { diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java index 4496a82e55d5..d82e5496443e 100644 --- a/media/java/android/media/tv/TvContract.java +++ b/media/java/android/media/tv/TvContract.java @@ -1624,7 +1624,12 @@ public final class TvContract { /** Column definitions for the TV channels table. */ public static final class Channels implements BaseTvColumns { - /** The content:// style URI for this table. */ + /** + * The content:// style URI for this table. + * + * <p>SQL selection is not supported for {@link ContentResolver#query}, + * {@link ContentResolver#update} and {@link ContentResolver#delete} operations. + */ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH_CHANNEL); @@ -2423,7 +2428,12 @@ public final class TvContract { */ public static final class Programs implements BaseTvColumns, ProgramColumns { - /** The content:// style URI for this table. */ + /** + * The content:// style URI for this table. + * + * <p>SQL selection is not supported for {@link ContentResolver#query}, + * {@link ContentResolver#update} and {@link ContentResolver#delete} operations. + */ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH_PROGRAM); @@ -2737,7 +2747,12 @@ public final class TvContract { */ public static final class RecordedPrograms implements BaseTvColumns, ProgramColumns { - /** The content:// style URI for this table. */ + /** + * The content:// style URI for this table. + * + * <p>SQL selection is not supported for {@link ContentResolver#query}, + * {@link ContentResolver#update} and {@link ContentResolver#delete} operations. + */ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH_RECORDED_PROGRAM); @@ -2858,7 +2873,12 @@ public final class TvContract { public static final class PreviewPrograms implements BaseTvColumns, ProgramColumns, PreviewProgramColumns { - /** The content:// style URI for this table. */ + /** + * The content:// style URI for this table. + * + * <p>SQL selection is not supported for {@link ContentResolver#query}, + * {@link ContentResolver#update} and {@link ContentResolver#delete} operations. + */ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH_PREVIEW_PROGRAM); @@ -2905,7 +2925,12 @@ public final class TvContract { public static final class WatchNextPrograms implements BaseTvColumns, ProgramColumns, PreviewProgramColumns { - /** The content:// style URI for this table. */ + /** + * The content:// style URI for this table. + * + * <p>SQL selection is not supported for {@link ContentResolver#query}, + * {@link ContentResolver#update} and {@link ContentResolver#delete} operations. + */ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH_WATCH_NEXT_PROGRAM); diff --git a/packages/SettingsLib/res/layout/preference_two_target.xml b/packages/SettingsLib/res/layout/preference_two_target.xml index 5446acebeeb3..9fb956ecf22e 100644 --- a/packages/SettingsLib/res/layout/preference_two_target.xml +++ b/packages/SettingsLib/res/layout/preference_two_target.xml @@ -37,7 +37,7 @@ android:id="@+id/icon_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:minWidth="60dp" + android:minWidth="56dp" android:orientation="horizontal" android:paddingEnd="12dp" android:paddingTop="4dp" diff --git a/packages/SystemUI/Android.mk b/packages/SystemUI/Android.mk index 5ee0c64c9591..2fd7e87a683e 100644 --- a/packages/SystemUI/Android.mk +++ b/packages/SystemUI/Android.mk @@ -31,6 +31,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-Iaidl-files-unde LOCAL_STATIC_ANDROID_LIBRARIES := \ SystemUIPluginLib \ + android-support-v4 \ android-support-v7-recyclerview \ android-support-v7-preference \ android-support-v7-appcompat \ diff --git a/packages/SystemUI/res/values/arrays_tv.xml b/packages/SystemUI/res/values/arrays_tv.xml index e52c5db9d227..7541b0e8c084 100644 --- a/packages/SystemUI/res/values/arrays_tv.xml +++ b/packages/SystemUI/res/values/arrays_tv.xml @@ -31,5 +31,6 @@ <item>com.google.android.katniss.setting/.SpeechSettingsActivity</item> <item>com.google.android.katniss.setting/.SearchSettingsActivity</item> <item>com.google.android.gsf.notouch/.UsageDiagnosticsSettingActivity</item> + <item>com.google.android.tvlauncher/.notifications.NotificationsSidePanelActivity</item> </string-array> </resources> diff --git a/packages/SystemUI/res/values/config_tv.xml b/packages/SystemUI/res/values/config_tv.xml index 40e3b128a55e..ffd58dcfd50d 100644 --- a/packages/SystemUI/res/values/config_tv.xml +++ b/packages/SystemUI/res/values/config_tv.xml @@ -17,17 +17,9 @@ <resources> <!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows, when the PIP menu is shown with settings. --> - <string translatable="false" name="pip_settings_bounds">"662 54 1142 324"</string> + <string translatable="false" name="pip_settings_bounds">"662 756 1142 1026"</string> <!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows, when the PIP menu is shown in center. --> <string translatable="false" name="pip_menu_bounds">"596 280 1324 690"</string> - - <!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows, - when the PIP is shown in Recents without focus. --> - <string translatable="false" name="pip_recents_bounds">"800 54 1120 234"</string> - - <!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows, - when the PIP is shown in Recents with focus. --> - <string translatable="false" name="pip_recents_focused_bounds">"775 54 1145 262"</string> </resources> diff --git a/packages/SystemUI/res/values/strings_tv.xml b/packages/SystemUI/res/values/strings_tv.xml index e578068163d7..a9bdb71de039 100644 --- a/packages/SystemUI/res/values/strings_tv.xml +++ b/packages/SystemUI/res/values/strings_tv.xml @@ -17,6 +17,14 @@ */ --> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <!-- Picture-in-Picture (PIP) notification --> + <!-- Title for the notification channel for TV PIP controls. [CHAR LIMIT=NONE] --> + <string name="notification_channel_tv_pip">Picture-in-Picture</string> + <!-- Title of the picture-in-picture (PIP) notification title + when the media doesn't have title [CHAR LIMIT=NONE] --> + <string name="pip_notification_unknown_title">(No title program)</string> + <!-- Picture-in-Picture (PIP) menu --> <eat-comment /> <!-- Button to close picture-in-picture (PIP) in PIP menu [CHAR LIMIT=30] --> diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java index 9735bfc666d7..6667b71efc0e 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java @@ -61,7 +61,8 @@ import static android.view.Display.DEFAULT_DISPLAY; */ public class PipManager implements BasePipManager { private static final String TAG = "PipManager"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final String SETTINGS_PACKAGE_AND_CLASS_DELIMITER = "/"; private static PipManager sPipManager; @@ -122,6 +123,7 @@ public class PipManager implements BasePipManager { private ComponentName mPipComponentName; private MediaController mPipMediaController; private String[] mLastPackagesResourceGranted; + private PipNotification mPipNotification; private final PinnedStackListener mPinnedStackListener = new PinnedStackListener(); @@ -246,6 +248,8 @@ public class PipManager implements BasePipManager { } catch (RemoteException e) { Log.e(TAG, "Failed to register pinned stack listener", e); } + + mPipNotification = new PipNotification(context); } private void loadConfigurationsAndApply() { @@ -267,6 +271,7 @@ public class PipManager implements BasePipManager { */ public void onConfigurationChanged() { loadConfigurationsAndApply(); + mPipNotification.onConfigurationChanged(mContext); } /** @@ -345,7 +350,7 @@ public class PipManager implements BasePipManager { * @param state In Pip state also used to determine the new size for the Pip. */ void resizePinnedStack(int state) { - if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + state); + if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + state, new Exception()); boolean wasStateNoPip = (mState == STATE_NO_PIP); mResumeResizePinnedStackRunnable = state; for (int i = mListeners.size() - 1; i >= 0; --i) { @@ -511,8 +516,8 @@ public class PipManager implements BasePipManager { /** * Returns the PIPed activity's playback state. - * This returns one of {@link PLAYBACK_STATE_PLAYING}, {@link PLAYBACK_STATE_PAUSED}, - * or {@link PLAYBACK_STATE_UNAVAILABLE}. + * This returns one of {@link #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, + * or {@link #PLAYBACK_STATE_UNAVAILABLE}. */ int getPlaybackState() { if (mPipMediaController == null || mPipMediaController.getPlaybackState() == null) { diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java new file mode 100644 index 000000000000..727eb5a3dba4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java @@ -0,0 +1,225 @@ +/* + * 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.pip.tv; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.Icon; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.PlaybackState; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; + +import com.android.systemui.util.NotificationChannels; +import com.android.systemui.R; +import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; + +/** + * A notification that informs users that PIP is running and also provides PIP controls. + * <p>Once it's created, it will manage the PIP notification UI by itself except for handling + * configuration changes. + */ +public class PipNotification { + private static final String TAG = "PipNotification"; + private static final boolean DEBUG = PipManager.DEBUG; + + private static final String ACTION_MENU = "PipNotification.menu"; + private static final String ACTION_CLOSE = "PipNotification.close"; + + private final PipManager mPipManager = PipManager.getInstance(); + + private final NotificationManager mNotificationManager; + private final Notification.Builder mNotificationBuilder; + + private MediaController mMediaController; + private String mDefaultTitle; + private Icon mDefaultIcon; + + private boolean mNotified; + private String mTitle; + private Bitmap mArt; + + private PipManager.Listener mPipListener = new PipManager.Listener() { + @Override + public void onPipEntered() { + updateMediaControllerMetadata(); + notifyPipNotification(); + } + + @Override + public void onPipActivityClosed() { + dismissPipNotification(); + } + + @Override + public void onShowPipMenu() { + // no-op. + } + + @Override + public void onMoveToFullscreen() { + dismissPipNotification(); + } + + @Override + public void onPipResizeAboutToStart() { + // no-op. + } + }; + + private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackState state) { + if (updateMediaControllerMetadata() && mNotified) { + // update notification + notifyPipNotification(); + } + } + }; + + private final PipManager.MediaListener mPipMediaListener = new PipManager.MediaListener() { + @Override + public void onMediaControllerChanged() { + MediaController newController = mPipManager.getMediaController(); + if (mMediaController == newController) { + return; + } + if (mMediaController != null) { + mMediaController.unregisterCallback(mMediaControllerCallback); + } + mMediaController = newController; + if (mMediaController != null) { + mMediaController.registerCallback(mMediaControllerCallback); + } + if (updateMediaControllerMetadata() && mNotified) { + // update notification + notifyPipNotification(); + } + } + }; + + private final BroadcastReceiver mEventReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) { + Log.d(TAG, "Received " + intent.getAction() + " from the notification UI"); + } + switch (intent.getAction()) { + case ACTION_MENU: + mPipManager.showPictureInPictureMenu(); + break; + case ACTION_CLOSE: + mPipManager.closePip(); + break; + } + } + }; + + public PipNotification(Context context) { + mNotificationManager = (NotificationManager) context.getSystemService( + Context.NOTIFICATION_SERVICE); + + mNotificationBuilder = new Notification.Builder(context, NotificationChannels.TVPIP) + .setLocalOnly(true) + .setOngoing(false) + .setCategory(Notification.CATEGORY_SYSTEM) + .extend(new Notification.TvExtender() + .setContentIntent(createPendingIntent(context, ACTION_MENU)) + .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE))); + + mPipManager.addListener(mPipListener); + mPipManager.addMediaListener(mPipMediaListener); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_MENU); + intentFilter.addAction(ACTION_CLOSE); + context.registerReceiver(mEventReceiver, intentFilter); + + onConfigurationChanged(context); + } + + /** + * Called by {@link PipManager} when the configuration is changed. + */ + void onConfigurationChanged(Context context) { + Resources res = context.getResources(); + mDefaultTitle = res.getString(R.string.pip_notification_unknown_title); + mDefaultIcon = Icon.createWithResource(context, + res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR + ? R.drawable.pip_expand_ll : R.drawable.pip_expand_lr); + if (mNotified) { + // update notification + notifyPipNotification(); + } + } + + private void notifyPipNotification() { + mNotified = true; + mNotificationBuilder + .setShowWhen(true) + .setWhen(System.currentTimeMillis()) + // TODO: Sending bitmap doesn't work in launcher side. Once launcher supports it, + // we can set icon. + //.setSmallIcon(mArt != null ? Icon.createWithBitmap(mArt) : mDefaultIcon) + .setSmallIcon(mDefaultIcon.getResId()) + .setContentTitle(!TextUtils.isEmpty(mTitle) ? mTitle : mDefaultTitle); + mNotificationManager.notify(SystemMessage.NOTE_TV_PIP, mNotificationBuilder.build()); + } + + private void dismissPipNotification() { + mNotified = false; + mNotificationManager.cancel(SystemMessage.NOTE_TV_PIP); + } + + private boolean updateMediaControllerMetadata() { + String title = null; + Bitmap art = null; + if (mPipManager.getMediaController() != null) { + MediaMetadata metadata = mPipManager.getMediaController().getMetadata(); + if (metadata != null) { + title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE); + if (TextUtils.isEmpty(title)) { + title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE); + } + art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); + if (art == null) { + art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART); + } + } + } + if (!TextUtils.equals(title, mTitle) || art != mArt) { + mTitle = title; + mArt = art; + return true; + } + return false; + } + + private static PendingIntent createPendingIntent(Context context, String action) { + return PendingIntent.getBroadcast(context, 0, + new Intent(action), PendingIntent.FLAG_CANCEL_CURRENT); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java index e34987b1d47a..4b614eda941a 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java @@ -362,11 +362,17 @@ public class TaskView extends FixedSizeFrameLayout implements Task.TaskCallbacks * Cancels any current transform animations. */ public void cancelTransformAnimation() { + cancelDimAnimationIfExists(); Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation); - Utilities.cancelAnimationWithoutCallbacks(mDimAnimator); Utilities.cancelAnimationWithoutCallbacks(mOutlineAnimator); } + private void cancelDimAnimationIfExists() { + if (mDimAnimator != null) { + mDimAnimator.cancel(); + } + } + /** Enables/disables handling touch on this task view. */ public void setTouchEnabled(boolean enabled) { setOnClickListener(enabled ? this : null); @@ -546,7 +552,7 @@ public class TaskView extends FixedSizeFrameLayout implements Task.TaskCallbacks @Override public void onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration, boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger) { - Utilities.cancelAnimationWithoutCallbacks(mDimAnimator); + cancelDimAnimationIfExists(); // Dim the view after the app window transitions down into recents postAnimationTrigger.increment(); diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java index da56e62f3c6e..90c65580c7fd 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java @@ -767,7 +767,7 @@ public class DividerView extends FrameLayout implements OnTouchListener, mDockedStackMinimized = minimized; } else if (mDockedStackMinimized != minimized) { mIsInMinimizeInteraction = true; - if (minimized) { + if (minimized && (mCurrentAnimator == null || !mCurrentAnimator.isRunning())) { mDividerPositionBeforeMinimized = getCurrentPosition(); } mMinimizedSnapAlgorithm = null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/Abortable.java b/packages/SystemUI/src/com/android/systemui/statusbar/Abortable.java new file mode 100644 index 000000000000..d5ec4f67e82d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/Abortable.java @@ -0,0 +1,24 @@ +/* + * 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; + +/** + * An interface that allows aborting existing operations. + */ +public interface Abortable { + void abort(); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java index d7eab9772677..b91561e01290 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java @@ -891,7 +891,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView * @return the calculated background color */ private int calculateBgColor(boolean withTint, boolean withOverRide) { - if (mDark) { + if (withTint && mDark) { return getContext().getColor(R.color.notification_material_background_dark_color); } if (withOverRide && mOverrideTint != NO_COLOR) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java index 8c1b334fe570..93687478fc86 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java @@ -355,7 +355,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView NotificationColorUtil.getInstance(mContext)); int color = StatusBarIconView.NO_COLOR; if (colorize) { - color = mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded()); + color = mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded(), + getBackgroundColorWithoutTint()); } expandedIcon.setStaticDrawableColor(color); } @@ -859,7 +860,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void updateNotificationColor() { mNotificationColor = NotificationColorUtil.resolveContrastColor(mContext, - getStatusBarNotification().getNotification().color); + getStatusBarNotification().getNotification().color, + getBackgroundColorWithoutTint()); } public HybridNotificationView getSingleLineView() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java index 540c39150188..f8bad053c3ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java @@ -33,7 +33,6 @@ import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; -import android.util.ArraySet; import android.view.View; import android.widget.ImageView; import android.widget.RemoteViews; @@ -43,7 +42,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.util.NotificationColorUtil; import com.android.systemui.statusbar.notification.InflationException; -import com.android.systemui.statusbar.notification.NotificationInflater; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.HeadsUpManager; @@ -86,7 +84,7 @@ public class NotificationData { public List<SnoozeCriterion> snoozeCriteria; private int mCachedContrastColor = COLOR_INVALID; private int mCachedContrastColorIsFor = COLOR_INVALID; - private ArraySet<AsyncTask> mRunningTasks = new ArraySet(); + private Abortable mRunningTask = null; public Entry(StatusBarNotification n) { this.key = n.getKey(); @@ -203,13 +201,15 @@ public class NotificationData { } } - public int getContrastedColor(Context context, boolean ambient) { - int rawColor = ambient ? Notification.COLOR_DEFAULT : + public int getContrastedColor(Context context, boolean isLowPriority, + int backgroundColor) { + int rawColor = isLowPriority ? Notification.COLOR_DEFAULT : notification.getNotification().color; if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) { return mCachedContrastColor; } - final int contrasted = NotificationColorUtil.resolveContrastColor(context, rawColor); + final int contrasted = NotificationColorUtil.resolveContrastColor(context, rawColor, + backgroundColor); mCachedContrastColorIsFor = rawColor; mCachedContrastColor = contrasted; return mCachedContrastColor; @@ -218,24 +218,26 @@ public class NotificationData { /** * Abort all existing inflation tasks */ - public void abortInflation() { - for (AsyncTask task : mRunningTasks) { - task.cancel(true /* mayInterruptIfRunning */); + public void abortTask() { + if (mRunningTask != null) { + mRunningTask.abort(); + mRunningTask = null; } - mRunningTasks.clear(); } - public void addInflationTask(AsyncTask asyncInflationTask) { - mRunningTasks.add(asyncInflationTask); + public void setInflationTask(Abortable abortableTask) { + // abort any existing inflation + abortTask(); + mRunningTask = abortableTask; } - public void onInflationTaskFinished(AsyncTask asyncInflationTask) { - mRunningTasks.remove(asyncInflationTask); + public void onInflationTaskFinished() { + mRunningTask = null; } @VisibleForTesting - public ArraySet<AsyncTask> getRunningTasks() { - return mRunningTasks; + public Abortable getRunningTask() { + return mRunningTask; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java index 4305bdef6fef..dc538dac6856 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMenuRow.java @@ -35,6 +35,7 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.service.notification.StatusBarNotification; import android.view.LayoutInflater; @@ -107,7 +108,7 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size); mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height); mIconPadding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding); - mHandler = new Handler(); + mHandler = new Handler(Looper.getMainLooper()); mMenuItems = new ArrayList<>(); mSnoozeItem = createSnoozeItem(context); mInfoItem = createInfoItem(context); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java index 7cfc767f89b7..f1c26cd2daa8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java @@ -16,19 +16,26 @@ package com.android.systemui.statusbar.notification; +import android.annotation.Nullable; import android.app.Notification; import android.content.Context; import android.os.AsyncTask; +import android.os.CancellationSignal; import android.service.notification.StatusBarNotification; import android.util.Log; import android.view.View; import android.widget.RemoteViews; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.R; +import com.android.systemui.statusbar.Abortable; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.NotificationContentView; import com.android.systemui.statusbar.NotificationData; import com.android.systemui.statusbar.phone.StatusBar; +import com.android.systemui.util.Assert; + +import java.util.HashMap; /** * A utility that inflates the right kind of contentView based on the state @@ -116,126 +123,303 @@ public class NotificationInflater { @VisibleForTesting void inflateNotificationViews(int reInflateFlags) { StatusBarNotification sbn = mRow.getEntry().notification; - new AsyncInflationTask(mRow.getContext(), sbn, reInflateFlags).execute(); + new AsyncInflationTask(sbn, reInflateFlags, mRow, mIsLowPriority, + mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient, + mCallback, mRemoteViewClickHandler).execute(); } @VisibleForTesting - void inflateNotificationViews(int reInflateFlags, + InflationProgress inflateNotificationViews(int reInflateFlags, Notification.Builder builder, Context packageContext) { - NotificationData.Entry entry = mRow.getEntry(); - NotificationContentView privateLayout = mRow.getPrivateLayout(); - NotificationContentView publicLayout = mRow.getPublicLayout(); + InflationProgress result = createRemoteViews(reInflateFlags, builder, mIsLowPriority, + mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, + mRedactAmbient, packageContext); + apply(result, reInflateFlags, mRow, mRedactAmbient, mRemoteViewClickHandler, null); + return result; + } - boolean isLowPriority = mIsLowPriority && !mIsChildInGroup; + private static InflationProgress createRemoteViews(int reInflateFlags, + Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup, + boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, + Context packageContext) { + InflationProgress result = new InflationProgress(); + isLowPriority = isLowPriority && !isChildInGroup; if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) { - final RemoteViews newContentView = createContentView(builder, - isLowPriority, mUsesIncreasedHeight); - if (!compareRemoteViews(newContentView, - entry.cachedContentView)) { - View contentViewLocal = newContentView.apply( - packageContext, - privateLayout, - mRemoteViewClickHandler); - contentViewLocal.setIsRootNamespace(true); - privateLayout.setContractedChild(contentViewLocal); - } else { - newContentView.reapply(packageContext, - privateLayout.getContractedChild(), - mRemoteViewClickHandler); - } - entry.cachedContentView = newContentView; + result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight); } if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) { - final RemoteViews newBigContentView = createBigContentView( - builder, isLowPriority); - if (newBigContentView != null) { - if (!compareRemoteViews(newBigContentView, entry.cachedBigContentView)) { - View bigContentViewLocal = newBigContentView.apply( - packageContext, - privateLayout, - mRemoteViewClickHandler); - bigContentViewLocal.setIsRootNamespace(true); - privateLayout.setExpandedChild(bigContentViewLocal); - } else { - newBigContentView.reapply(packageContext, - privateLayout.getExpandedChild(), - mRemoteViewClickHandler); - } - } else if (entry.cachedBigContentView != null) { - privateLayout.setExpandedChild(null); - } - entry.cachedBigContentView = newBigContentView; - mRow.setExpandable(newBigContentView != null); + result.newExpandedView = createExpandedView(builder, isLowPriority); } if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) { - final RemoteViews newHeadsUpContentView = - builder.createHeadsUpContentView(mUsesIncreasedHeadsUpHeight); - if (newHeadsUpContentView != null) { - if (!compareRemoteViews(newHeadsUpContentView, - entry.cachedHeadsUpContentView)) { - View headsUpContentViewLocal = newHeadsUpContentView.apply( - packageContext, - privateLayout, - mRemoteViewClickHandler); - headsUpContentViewLocal.setIsRootNamespace(true); - privateLayout.setHeadsUpChild(headsUpContentViewLocal); - } else { - newHeadsUpContentView.reapply(packageContext, - privateLayout.getHeadsUpChild(), - mRemoteViewClickHandler); - } - } else if (entry.cachedHeadsUpContentView != null) { - privateLayout.setHeadsUpChild(null); - } - entry.cachedHeadsUpContentView = newHeadsUpContentView; + result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight); } if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) { - final RemoteViews newPublicNotification - = builder.makePublicContentView(); - if (!compareRemoteViews(newPublicNotification, entry.cachedPublicContentView)) { - View publicContentView = newPublicNotification.apply( - packageContext, - publicLayout, - mRemoteViewClickHandler); - publicContentView.setIsRootNamespace(true); - publicLayout.setContractedChild(publicContentView); - } else { - newPublicNotification.reapply(packageContext, - publicLayout.getContractedChild(), - mRemoteViewClickHandler); - } - entry.cachedPublicContentView = newPublicNotification; + result.newPublicView = builder.makePublicContentView(); } if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) { - final RemoteViews newAmbientNotification = mRedactAmbient - ? builder.makePublicAmbientNotification() + result.newAmbientView = redactAmbient ? builder.makePublicAmbientNotification() : builder.makeAmbientNotification(); - NotificationContentView newParent = mRedactAmbient ? publicLayout : privateLayout; - NotificationContentView otherParent = !mRedactAmbient ? publicLayout : privateLayout; - - if (newParent.getAmbientChild() == null || - !compareRemoteViews(newAmbientNotification, entry.cachedAmbientContentView)) { - View ambientContentView = newAmbientNotification.apply( - packageContext, - newParent, - mRemoteViewClickHandler); - ambientContentView.setIsRootNamespace(true); - newParent.setAmbientChild(ambientContentView); - otherParent.setAmbientChild(null); - } else { - newAmbientNotification.reapply(packageContext, - newParent.getAmbientChild(), - mRemoteViewClickHandler); + } + result.packageContext = packageContext; + return result; + } + + public static CancellationSignal apply(InflationProgress result, int reInflateFlags, + ExpandableNotificationRow row, boolean redactAmbient, + RemoteViews.OnClickHandler remoteViewClickHandler, + @Nullable InflationCallback callback) { + NotificationData.Entry entry = row.getEntry(); + NotificationContentView privateLayout = row.getPrivateLayout(); + NotificationContentView publicLayout = row.getPublicLayout(); + final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>(); + + int flag = FLAG_REINFLATE_CONTENT_VIEW; + if ((reInflateFlags & flag) != 0) { + boolean isNewView = !compareRemoteViews(result.newContentView, entry.cachedContentView); + ApplyCallback applyCallback = new ApplyCallback() { + @Override + public void setResultView(View v) { + result.inflatedContentView = v; + } + + @Override + public RemoteViews getRemoteView() { + return result.newContentView; + } + }; + applyRemoteView(result, reInflateFlags, flag, row, redactAmbient, + isNewView, remoteViewClickHandler, callback, entry, privateLayout, + privateLayout.getContractedChild(), + runningInflations, applyCallback); + } + + flag = FLAG_REINFLATE_EXPANDED_VIEW; + if ((reInflateFlags & flag) != 0) { + if (result.newExpandedView != null) { + boolean isNewView = !compareRemoteViews(result.newExpandedView, + entry.cachedBigContentView); + ApplyCallback applyCallback = new ApplyCallback() { + @Override + public void setResultView(View v) { + result.inflatedExpandedView = v; + } + + @Override + public RemoteViews getRemoteView() { + return result.newExpandedView; + } + }; + applyRemoteView(result, reInflateFlags, flag, row, + redactAmbient, isNewView, remoteViewClickHandler, callback, entry, + privateLayout, privateLayout.getExpandedChild(), runningInflations, + applyCallback); } - entry.cachedAmbientContentView = newAmbientNotification; } + + flag = FLAG_REINFLATE_HEADS_UP_VIEW; + if ((reInflateFlags & flag) != 0) { + if (result.newHeadsUpView != null) { + boolean isNewView = !compareRemoteViews(result.newHeadsUpView, + entry.cachedHeadsUpContentView); + ApplyCallback applyCallback = new ApplyCallback() { + @Override + public void setResultView(View v) { + result.inflatedHeadsUpView = v; + } + + @Override + public RemoteViews getRemoteView() { + return result.newHeadsUpView; + } + }; + applyRemoteView(result, reInflateFlags, flag, row, + redactAmbient, isNewView, remoteViewClickHandler, callback, entry, + privateLayout, privateLayout.getHeadsUpChild(), runningInflations, + applyCallback); + } + } + + flag = FLAG_REINFLATE_PUBLIC_VIEW; + if ((reInflateFlags & flag) != 0) { + boolean isNewView = !compareRemoteViews(result.newPublicView, + entry.cachedPublicContentView); + ApplyCallback applyCallback = new ApplyCallback() { + @Override + public void setResultView(View v) { + result.inflatedPublicView = v; + } + + @Override + public RemoteViews getRemoteView() { + return result.newPublicView; + } + }; + applyRemoteView(result, reInflateFlags, flag, row, + redactAmbient, isNewView, remoteViewClickHandler, callback, entry, + publicLayout, publicLayout.getContractedChild(), runningInflations, + applyCallback); + } + + flag = FLAG_REINFLATE_AMBIENT_VIEW; + if ((reInflateFlags & flag) != 0) { + NotificationContentView newParent = redactAmbient ? publicLayout : privateLayout; + boolean isNewView = !canReapplyAmbient(row, redactAmbient) || + !compareRemoteViews(result.newAmbientView, entry.cachedAmbientContentView); + ApplyCallback applyCallback = new ApplyCallback() { + @Override + public void setResultView(View v) { + result.inflatedAmbientView = v; + } + + @Override + public RemoteViews getRemoteView() { + return result.newAmbientView; + } + }; + applyRemoteView(result, reInflateFlags, flag, row, + redactAmbient, isNewView, remoteViewClickHandler, callback, entry, + newParent, newParent.getAmbientChild(), runningInflations, + applyCallback); + } + + // Let's try to finish, maybe nobody is even inflating anything + finishIfDone(result, reInflateFlags, runningInflations, callback, row, + redactAmbient); + CancellationSignal cancellationSignal = new CancellationSignal(); + cancellationSignal.setOnCancelListener( + () -> runningInflations.values().forEach(CancellationSignal::cancel)); + return cancellationSignal; + } + + private static void applyRemoteView(final InflationProgress result, + final int reInflateFlags, int inflationId, + final ExpandableNotificationRow row, + final boolean redactAmbient, boolean isNewView, + RemoteViews.OnClickHandler remoteViewClickHandler, + @Nullable final InflationCallback callback, NotificationData.Entry entry, + NotificationContentView parentLayout, View existingView, + final HashMap<Integer, CancellationSignal> runningInflations, + ApplyCallback applyCallback) { + RemoteViews.OnViewAppliedListener listener + = new RemoteViews.OnViewAppliedListener() { + + @Override + public void onViewApplied(View v) { + if (isNewView) { + v.setIsRootNamespace(true); + applyCallback.setResultView(v); + } + runningInflations.remove(inflationId); + finishIfDone(result, reInflateFlags, runningInflations, callback, row, + redactAmbient); + } + + @Override + public void onError(Exception e) { + runningInflations.remove(inflationId); + handleInflationError(runningInflations, e, entry.notification, callback); + } + }; + CancellationSignal cancellationSignal; + RemoteViews newContentView = applyCallback.getRemoteView(); + if (isNewView) { + cancellationSignal = newContentView.applyAsync( + result.packageContext, + parentLayout, + null /* executor */, + listener, + remoteViewClickHandler); + } else { + cancellationSignal = newContentView.reapplyAsync( + result.packageContext, + existingView, + null /* executor */, + listener, + remoteViewClickHandler); + } + runningInflations.put(inflationId, cancellationSignal); } - private RemoteViews createBigContentView(Notification.Builder builder, + private static void handleInflationError(HashMap<Integer, CancellationSignal> runningInflations, + Exception e, StatusBarNotification notification, @Nullable InflationCallback callback) { + Assert.isMainThread(); + runningInflations.values().forEach(CancellationSignal::cancel); + if (callback != null) { + callback.handleInflationException(notification, e); + } + } + + /** + * Finish the inflation of the views + * + * @return true if the inflation was finished + */ + private static boolean finishIfDone(InflationProgress result, int reInflateFlags, + HashMap<Integer, CancellationSignal> runningInflations, + @Nullable InflationCallback endListener, ExpandableNotificationRow row, + boolean redactAmbient) { + Assert.isMainThread(); + NotificationData.Entry entry = row.getEntry(); + NotificationContentView privateLayout = row.getPrivateLayout(); + NotificationContentView publicLayout = row.getPublicLayout(); + if (runningInflations.isEmpty()) { + if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) { + if (result.inflatedContentView != null) { + privateLayout.setContractedChild(result.inflatedContentView); + } + entry.cachedContentView = result.newContentView; + } + + if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) { + if (result.inflatedExpandedView != null) { + privateLayout.setExpandedChild(result.inflatedExpandedView); + } else if (result.newExpandedView == null) { + privateLayout.setExpandedChild(null); + } + entry.cachedBigContentView = result.newExpandedView; + row.setExpandable(result.newExpandedView != null); + } + + if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) { + if (result.inflatedHeadsUpView != null) { + privateLayout.setHeadsUpChild(result.inflatedHeadsUpView); + } else if (result.newHeadsUpView == null) { + privateLayout.setHeadsUpChild(null); + } + entry.cachedHeadsUpContentView = result.newHeadsUpView; + } + + if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) { + if (result.inflatedPublicView != null) { + publicLayout.setContractedChild(result.inflatedPublicView); + } + entry.cachedPublicContentView = result.newPublicView; + } + + if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) { + if (result.inflatedAmbientView != null) { + NotificationContentView newParent = redactAmbient + ? publicLayout : privateLayout; + NotificationContentView otherParent = !redactAmbient + ? publicLayout : privateLayout; + newParent.setAmbientChild(result.inflatedAmbientView); + otherParent.setAmbientChild(null); + } + entry.cachedAmbientContentView = result.newAmbientView; + } + if (endListener != null) { + endListener.onAsyncInflationFinished(row.getEntry()); + } + return true; + } + return false; + } + + private static RemoteViews createExpandedView(Notification.Builder builder, boolean isLowPriority) { RemoteViews bigContentView = builder.createBigContentView(); if (bigContentView != null) { @@ -249,7 +433,7 @@ public class NotificationInflater { return null; } - private RemoteViews createContentView(Notification.Builder builder, + private static RemoteViews createContentView(Notification.Builder builder, boolean isLowPriority, boolean useLarge) { if (isLowPriority) { return builder.makeLowPriorityContentView(false /* useRegularSubtext */); @@ -258,7 +442,7 @@ public class NotificationInflater { } // Returns true if the RemoteViews are the same. - private boolean compareRemoteViews(final RemoteViews a, final RemoteViews b) { + private static boolean compareRemoteViews(final RemoteViews a, final RemoteViews b) { return (a == null && b == null) || (a != null && b != null && b.getPackage() != null @@ -272,7 +456,7 @@ public class NotificationInflater { } public interface InflationCallback { - void handleInflationException(StatusBarNotification notification, InflationException e); + void handleInflationException(StatusBarNotification notification, Exception e); void onAsyncInflationFinished(NotificationData.Entry entry); } @@ -286,37 +470,73 @@ public class NotificationInflater { inflateNotificationViews(); } - private class AsyncInflationTask extends AsyncTask<Void, Void, Notification.Builder> { + private static boolean canReapplyAmbient(ExpandableNotificationRow row, boolean redactAmbient) { + NotificationContentView ambientView = redactAmbient ? row.getPublicLayout() + : row.getPrivateLayout(); ; + return ambientView.getAmbientChild() != null; + } + + public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress> + implements InflationCallback, Abortable { private final StatusBarNotification mSbn; private final Context mContext; private final int mReInflateFlags; - private Context mPackageContext = null; + private final boolean mIsLowPriority; + private final boolean mIsChildInGroup; + private final boolean mUsesIncreasedHeight; + private final InflationCallback mCallback; + private final boolean mUsesIncreasedHeadsUpHeight; + private final boolean mRedactAmbient; + private ExpandableNotificationRow mRow; private Exception mError; - - private AsyncInflationTask(Context context, StatusBarNotification notification, - int reInflateFlags) { + private RemoteViews.OnClickHandler mRemoteViewClickHandler; + private CancellationSignal mCancellationSignal; + + private AsyncInflationTask(StatusBarNotification notification, + int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority, + boolean isChildInGroup, boolean usesIncreasedHeight, + boolean usesIncreasedHeadsUpHeight, boolean redactAmbient, + InflationCallback callback, + RemoteViews.OnClickHandler remoteViewClickHandler) { + mRow = row; + NotificationData.Entry entry = row.getEntry(); + entry.setInflationTask(this); mSbn = notification; - mContext = context; mReInflateFlags = reInflateFlags; - mRow.getEntry().addInflationTask(this); + mContext = mRow.getContext(); + mIsLowPriority = isLowPriority; + mIsChildInGroup = isChildInGroup; + mUsesIncreasedHeight = usesIncreasedHeight; + mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; + mRedactAmbient = redactAmbient; + mRemoteViewClickHandler = remoteViewClickHandler; + mCallback = callback; } @Override - protected Notification.Builder doInBackground(Void... params) { + protected InflationProgress doInBackground(Void... params) { try { final Notification.Builder recoveredBuilder = Notification.Builder.recoverBuilder(mContext, mSbn.getNotification()); - mPackageContext = mSbn.getPackageContext(mContext); + Context packageContext = mSbn.getPackageContext(mContext); Notification notification = mSbn.getNotification(); + if (mIsLowPriority) { + int backgroundColor = mContext.getColor( + R.color.notification_material_background_low_priority_color); + recoveredBuilder.setBackgroundColorHint(backgroundColor); + } if (notification.isMediaNotification()) { MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext, - mPackageContext); + packageContext); processor.setIsLowPriority(mIsLowPriority); processor.processNotification(notification, recoveredBuilder); } - return recoveredBuilder; + return createRemoteViews(mReInflateFlags, + recoveredBuilder, mIsLowPriority, mIsChildInGroup, + mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient, + packageContext); } catch (Exception e) { mError = e; return null; @@ -324,34 +544,64 @@ public class NotificationInflater { } @Override - protected void onPostExecute(Notification.Builder builder) { - mRow.getEntry().onInflationTaskFinished(this); + protected void onPostExecute(InflationProgress result) { if (mError == null) { - finishInflation(mReInflateFlags, builder, mPackageContext); + mCancellationSignal = apply(result, mReInflateFlags, mRow, mRedactAmbient, + mRemoteViewClickHandler, this); } else { handleError(mError); } } - } - private void finishInflation(int reinflationFlags, Notification.Builder builder, - Context context) { - try { - inflateNotificationViews(reinflationFlags, builder, context); - } catch (RuntimeException e){ + private void handleError(Exception e) { + mRow.getEntry().onInflationTaskFinished(); + StatusBarNotification sbn = mRow.getStatusBarNotification(); + final String ident = sbn.getPackageName() + "/0x" + + Integer.toHexString(sbn.getId()); + Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e); + mCallback.handleInflationException(sbn, + new InflationException("Couldn't inflate contentViews" + e)); + } + + @Override + public void abort() { + cancel(true /* mayInterruptIfRunning */); + if (mCancellationSignal != null) { + mCancellationSignal.cancel(); + } + } + + @Override + public void handleInflationException(StatusBarNotification notification, Exception e) { handleError(e); - return; } - mRow.onNotificationUpdated(); - mCallback.onAsyncInflationFinished(mRow.getEntry()); + + @Override + public void onAsyncInflationFinished(NotificationData.Entry entry) { + mRow.getEntry().onInflationTaskFinished(); + mRow.onNotificationUpdated(); + mCallback.onAsyncInflationFinished(mRow.getEntry()); + } + } + + private static class InflationProgress { + private RemoteViews newContentView; + private RemoteViews newHeadsUpView; + private RemoteViews newExpandedView; + private RemoteViews newAmbientView; + private RemoteViews newPublicView; + + private Context packageContext; + + private View inflatedContentView; + private View inflatedHeadsUpView; + private View inflatedExpandedView; + private View inflatedAmbientView; + private View inflatedPublicView; } - private void handleError(Exception e) { - StatusBarNotification sbn = mRow.getStatusBarNotification(); - final String ident = sbn.getPackageName() + "/0x" - + Integer.toHexString(sbn.getId()); - Log.e(StatusBar.TAG, "couldn't inflate view for notification " + ident, e); - mCallback.handleInflationException(sbn, - new InflationException("Couldn't inflate contentViews" + e)); + private abstract static class ApplyCallback { + public abstract void setResultView(View v); + public abstract RemoteViews getRemoteView(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/RowInflaterTask.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/RowInflaterTask.java new file mode 100644 index 000000000000..1bfc0cc6a6df --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/RowInflaterTask.java @@ -0,0 +1,65 @@ +/* + * 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.notification; + +import android.content.Context; +import android.support.v4.view.AsyncLayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.systemui.R; +import com.android.systemui.statusbar.Abortable; +import com.android.systemui.statusbar.ExpandableNotificationRow; +import com.android.systemui.statusbar.NotificationData; + +/** + * An inflater task that asynchronously inflates a ExpandableNotificationRow + */ +public class RowInflaterTask implements Abortable, AsyncLayoutInflater.OnInflateFinishedListener { + private RowInflationFinishedListener mListener; + private NotificationData.Entry mEntry; + private boolean mCancelled; + + /** + * Inflates a new notificationView. This should not be called twice on this object + */ + public void inflate(Context context, ViewGroup parent, NotificationData.Entry entry, + RowInflationFinishedListener listener) { + mListener = listener; + AsyncLayoutInflater inflater = new AsyncLayoutInflater(context); + mEntry = entry; + entry.setInflationTask(this); + inflater.inflate(R.layout.status_bar_notification_row, parent, this); + } + + @Override + public void abort() { + mCancelled = true; + } + + @Override + public void onInflateFinished(View view, int resid, ViewGroup parent) { + if (!mCancelled) { + mEntry.onInflationTaskFinished(); + mListener.onInflationFinished((ExpandableNotificationRow) view); + } + } + + public interface RowInflationFinishedListener { + void onInflationFinished(ExpandableNotificationRow row); + } +} 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 5690495c47b3..41fb5f7c220c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -579,6 +579,7 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks, .setComponent(aiaComponent) .setAction(Intent.ACTION_VIEW) .addCategory(Intent.CATEGORY_BROWSABLE) + .addCategory("unique:" + System.currentTimeMillis()) .putExtra(Intent.EXTRA_PACKAGE_NAME, appInfo.packageName) .putExtra(Intent.EXTRA_VERSION_CODE, appInfo.versionCode) .putExtra(Intent.EXTRA_EPHEMERAL_FAILURE, pendingIntent); 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 badbcb3da845..4610bc8fe54a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -172,6 +172,7 @@ import com.android.systemui.statusbar.ScrimView; import com.android.systemui.statusbar.SignalClusterView; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.InflationException; +import com.android.systemui.statusbar.notification.RowInflaterTask; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager; import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener; @@ -1588,12 +1589,12 @@ public class StatusBar extends SystemUI implements DemoMode, private void abortExistingInflation(String key) { if (mPendingNotifications.containsKey(key)) { Entry entry = mPendingNotifications.get(key); - entry.abortInflation(); + entry.abortTask(); mPendingNotifications.remove(key); } Entry addedEntry = mNotificationData.get(key); if (addedEntry != null) { - addedEntry.abortInflation(); + addedEntry.abortTask(); } } @@ -1610,7 +1611,7 @@ public class StatusBar extends SystemUI implements DemoMode, } @Override - public void handleInflationException(StatusBarNotification notification, InflationException e) { + public void handleInflationException(StatusBarNotification notification, Exception e) { handleNotificationError(notification, e.getMessage()); } @@ -6172,50 +6173,57 @@ public class StatusBar extends SystemUI implements DemoMode, entry.notification.getUser().getIdentifier()); final StatusBarNotification sbn = entry.notification; - ExpandableNotificationRow row; if (entry.row != null) { - row = entry.row; entry.reset(); + updateNotification(entry, pmUser, sbn, entry.row); } else { - // create the row view - LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - row = (ExpandableNotificationRow) inflater.inflate(R.layout.status_bar_notification_row, - parent, false); - row.setExpansionLogger(this, entry.notification.getKey()); - row.setGroupManager(mGroupManager); - row.setHeadsUpManager(mHeadsUpManager); - row.setRemoteInputController(mRemoteInputController); - row.setOnExpandClickListener(this); - row.setRemoteViewClickHandler(mOnClickHandler); - row.setInflationCallback(this); - - // Get the app name. - // Note that Notification.Builder#bindHeaderAppName has similar logic - // but since this field is used in the guts, it must be accurate. - // Therefore we will only show the application label, or, failing that, the - // package name. No substitutions. - final String pkg = sbn.getPackageName(); - String appname = pkg; - try { - final ApplicationInfo info = pmUser.getApplicationInfo(pkg, - PackageManager.MATCH_UNINSTALLED_PACKAGES - | PackageManager.MATCH_DISABLED_COMPONENTS); - if (info != null) { - appname = String.valueOf(pmUser.getApplicationLabel(info)); - } - } catch (NameNotFoundException e) { - // Do nothing - } - row.setAppName(appname); - row.setOnDismissRunnable(() -> - performRemoveNotification(row.getStatusBarNotification())); - row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); - if (ENABLE_REMOTE_INPUT) { - row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); + new RowInflaterTask().inflate(mContext, parent, entry, + row -> { + bindRow(entry, pmUser, sbn, row); + updateNotification(entry, pmUser, sbn, row); + }); + } + + } + + private void bindRow(Entry entry, PackageManager pmUser, + StatusBarNotification sbn, ExpandableNotificationRow row) { + row.setExpansionLogger(this, entry.notification.getKey()); + row.setGroupManager(mGroupManager); + row.setHeadsUpManager(mHeadsUpManager); + row.setRemoteInputController(mRemoteInputController); + row.setOnExpandClickListener(this); + row.setRemoteViewClickHandler(mOnClickHandler); + row.setInflationCallback(this); + + // Get the app name. + // Note that Notification.Builder#bindHeaderAppName has similar logic + // but since this field is used in the guts, it must be accurate. + // Therefore we will only show the application label, or, failing that, the + // package name. No substitutions. + final String pkg = sbn.getPackageName(); + String appname = pkg; + try { + final ApplicationInfo info = pmUser.getApplicationInfo(pkg, + PackageManager.MATCH_UNINSTALLED_PACKAGES + | PackageManager.MATCH_DISABLED_COMPONENTS); + if (info != null) { + appname = String.valueOf(pmUser.getApplicationLabel(info)); } + } catch (NameNotFoundException e) { + // Do nothing + } + row.setAppName(appname); + row.setOnDismissRunnable(() -> + performRemoveNotification(row.getStatusBarNotification())); + row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); + if (ENABLE_REMOTE_INPUT) { + row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); } + } + private void updateNotification(Entry entry, PackageManager pmUser, + StatusBarNotification sbn, ExpandableNotificationRow row) { row.setNeedsRedaction(needsRedaction(entry)); boolean isLowPriority = mNotificationData.isAmbient(sbn.getKey()); row.setIsLowPriority(isLowPriority); diff --git a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java index cd85a760159b..ae8afe4dcad7 100644 --- a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java +++ b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java @@ -30,6 +30,7 @@ public class NotificationChannels extends SystemUI { public static String SCREENSHOTS = "SCN"; public static String GENERAL = "GEN"; public static String STORAGE = "DSK"; + public static String TVPIP = "TPP"; @VisibleForTesting static void createAll(Context context) { @@ -55,6 +56,15 @@ public class NotificationChannels extends SystemUI { ? NotificationManager.IMPORTANCE_DEFAULT : NotificationManager.IMPORTANCE_LOW) )); + if (isTv(context)) { + // TV specific notification channel for TV PIP controls. + // Importance should be {@link NotificationManager#IMPORTANCE_MAX} to have the highest + // priority, so it can be shown in all times. + nm.createNotificationChannel(new NotificationChannel( + TVPIP, + context.getString(R.string.notification_channel_tv_pip), + NotificationManager.IMPORTANCE_MAX)); + } } @Override diff --git a/packages/SystemUI/tests/Android.mk b/packages/SystemUI/tests/Android.mk index 5e8b3f905258..5e71dd4684c5 100644 --- a/packages/SystemUI/tests/Android.mk +++ b/packages/SystemUI/tests/Android.mk @@ -38,6 +38,7 @@ LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res \ LOCAL_STATIC_ANDROID_LIBRARIES := \ SystemUIPluginLib \ + android-support-v4 \ android-support-v7-recyclerview \ android-support-v7-preference \ android-support-v7-appcompat \ diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationInflaterTest.java index fbb25e5484ba..15381b7e9425 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationInflaterTest.java @@ -26,6 +26,7 @@ import android.app.Notification; import android.content.Context; import android.service.notification.StatusBarNotification; import android.support.test.InstrumentationRegistry; +import android.support.test.annotation.UiThreadTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.widget.RemoteViews; @@ -41,7 +42,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.concurrent.CountDownLatch; -import java.util.function.Function; @SmallTest @RunWith(AndroidJUnit4.class) @@ -67,7 +67,7 @@ public class NotificationInflaterTest { mNotificationInflater.setInflationCallback(new NotificationInflater.InflationCallback() { @Override public void handleInflationException(StatusBarNotification notification, - InflationException e) { + Exception e) { } @Override @@ -77,6 +77,7 @@ public class NotificationInflaterTest { } @Test + @UiThreadTest public void testIncreasedHeadsUpBeingUsed() { mNotificationInflater.setUsesIncreasedHeadsUpHeight(true); Notification.Builder builder = spy(mBuilder); @@ -85,6 +86,7 @@ public class NotificationInflaterTest { } @Test + @UiThreadTest public void testIncreasedHeightBeingUsed() { mNotificationInflater.setUsesIncreasedHeight(true); Notification.Builder builder = spy(mBuilder); @@ -124,10 +126,10 @@ public class NotificationInflaterTest { @Test public void testAsyncTaskRemoved() throws Exception { - mRow.getEntry().abortInflation(); + mRow.getEntry().abortTask(); runThenWaitForInflation(() -> mNotificationInflater.inflateNotificationViews(), mNotificationInflater); - Assert.assertTrue(mRow.getEntry().getRunningTasks().size() == 0); + Assert.assertNull(mRow.getEntry().getRunningTask() ); } public static void runThenWaitForInflation(Runnable block, @@ -143,7 +145,7 @@ public class NotificationInflaterTest { inflater.setInflationCallback(new NotificationInflater.InflationCallback() { @Override public void handleInflationException(StatusBarNotification notification, - InflationException e) { + Exception e) { if (!expectingException) { exceptionHolder.setException(e); } diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto index 53b3fe9c1a0d..2f6b7e619e68 100644 --- a/proto/src/system_messages.proto +++ b/proto/src/system_messages.proto @@ -212,6 +212,10 @@ message SystemMessage { // Package: com.android.systemui NOTE_LOGOUT_USER = 1011; + // Notify the user that a TV PIP is running. + // Package: com.android.systemui + NOTE_TV_PIP = 1100; + // Communicate to the user about remote bugreports. // Package: android NOTE_REMOTE_BUGREPORT = 678432343; diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java index cbf97ddc8abb..8d947b92159c 100644 --- a/services/autofill/java/com/android/server/autofill/Helper.java +++ b/services/autofill/java/com/android/server/autofill/Helper.java @@ -20,9 +20,7 @@ import android.annotation.NonNull; import android.app.assist.AssistStructure; import android.app.assist.AssistStructure.ViewNode; import android.os.Bundle; -import android.util.DebugUtils; import android.view.autofill.AutofillId; -import android.view.autofill.AutofillManager; import java.util.Arrays; import java.util.Objects; @@ -68,10 +66,6 @@ public final class Helper { return builder.toString(); } - static String getUpdateActionAsString(int action) { - return DebugUtils.flagsToString(AutofillManager.class, "ACTION_", action); - } - static ViewNode findViewNodeById(@NonNull AssistStructure structure, @NonNull AutofillId id) { final int size = structure.getWindowNodeCount(); for (int i = 0; i < size; i++) { diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 3c85034e98ac..a98821de56ba 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -26,7 +26,6 @@ import static android.view.autofill.AutofillManager.ACTION_VIEW_ENTERED; import static android.view.autofill.AutofillManager.ACTION_VIEW_EXITED; import static com.android.server.autofill.Helper.findViewNodeById; -import static com.android.server.autofill.Helper.getUpdateActionAsString; import static com.android.server.autofill.Helper.sDebug; import static com.android.server.autofill.Helper.sVerbose; import static com.android.server.autofill.ViewState.STATE_AUTOFILLED; @@ -689,6 +688,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } else { final Parcelable result = data.getParcelable( AutofillManager.EXTRA_AUTHENTICATION_RESULT); + if (sVerbose) Slog.d(TAG, "setAuthenticationResultLocked() for " + result); + if (result instanceof FillResponse) { FillResponse response = (FillResponse) result; @@ -697,6 +698,16 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mResponseWaitingAuth = null; if (requestIndex >= 0) { response.setRequestId(mResponses.keyAt(requestIndex)); + if (response.getDatasets() == null || response.getDatasets().isEmpty()) { + // TODO(b/37424539): there is a race condition that causes the authentication + // dialog to be shown again after the service authreplied with a no-datasets + // response. We're fixing it by hiding the UI when that happens, but that + // sounds like a hack - hopefully the real problem will go away when we + // refactor auth to support partitions; if it doesn't, we need to + // investigate it further (it can be reproduced by running + // LoginActivityTest.testFillResponseAuthServiceHasNoData()) + mUi.hideAll(); + } processResponseLocked(response); } else { Slog.e(TAG, "Error cannot find id for auth response"); @@ -1005,18 +1016,14 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } if (sVerbose) { - Slog.v(TAG, "updateLocked(): id=" + id + ", action=" + getUpdateActionAsString(action) - + ", flags=" + flags); + Slog.v(TAG, "updateLocked(): id=" + id + ", action=" + action + ", flags=" + flags); } ViewState viewState = mViewStates.get(id); if (viewState == null) { if (action == ACTION_START_SESSION || action == ACTION_VALUE_CHANGED || action == ACTION_VIEW_ENTERED) { - if (sVerbose) { - Slog.v(TAG, - "Creating viewState for " + id + " on " + getActionAsString(action)); - } + if (sVerbose) Slog.v(TAG, "Creating viewState for " + id + " on " + action); boolean isIgnored = isIgnoredLocked(id); viewState = new ViewState(this, id, value, this, isIgnored ? ViewState.STATE_IGNORED : ViewState.STATE_INITIAL); @@ -1026,7 +1033,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } } else { - if (sVerbose) Slog.v(TAG, "Ignored " + getActionAsString(action) + " for " + id); + if (sVerbose) Slog.v(TAG, "Ignored action " + action + " for " + id); return; } } @@ -1078,6 +1085,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState break; case ACTION_VIEW_EXITED: if (mCurrentViewId == viewState.id) { + if (sVerbose) Slog.d(TAG, "Exiting view " + id); mUi.hideFillUi(viewState.id); mCurrentViewId = null; } @@ -1118,10 +1126,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState getUiForShowing().showFillUi(filledId, response, filterText, mPackageName); } - String getActionAsString(int flag) { - return DebugUtils.flagsToString(AutofillManager.class, "ACTION_", flag); - } - boolean isDestroyed() { synchronized (mLock) { return mDestroyed; @@ -1180,14 +1184,15 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } private void processResponseLocked(@NonNull FillResponse response) { + final int requestId = response.getRequestId(); if (sVerbose) { - Slog.v(TAG, "processResponseLocked(mCurrentViewId=" + mCurrentViewId + "):" + response); + Slog.v(TAG, "processResponseLocked(): mCurrentViewId=" + mCurrentViewId + + ", reqId=" + requestId + ", resp=" + response); } if (mResponses == null) { mResponses = new SparseArray<>(4); } - final int requestId = response.getRequestId(); mResponses.put(requestId, response); mClientState = response.getClientState(); diff --git a/services/autofill/java/com/android/server/autofill/ui/FillUi.java b/services/autofill/java/com/android/server/autofill/ui/FillUi.java index 922962f1b63b..31b4b55818ac 100644 --- a/services/autofill/java/com/android/server/autofill/ui/FillUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/FillUi.java @@ -16,6 +16,7 @@ package com.android.server.autofill.ui; import static com.android.server.autofill.Helper.sDebug; +import static com.android.server.autofill.Helper.sVerbose; import android.annotation.NonNull; import android.annotation.Nullable; @@ -265,8 +266,11 @@ final class FillUi { mContentWidth = 0; mContentHeight = 0; - final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0); - final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0); + final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.x, + MeasureSpec.AT_MOST); + final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxSize.y, + MeasureSpec.AT_MOST); + final int itemCount = Math.min(mAdapter.getCount(), VISIBLE_OPTIONS_MAX_COUNT); for (int i = 0; i < itemCount; i++) { View view = mAdapter.getItem(i).getView(); @@ -334,6 +338,11 @@ final class FillUi { @Override public void show(WindowManager.LayoutParams p, Rect transitionEpicenter, boolean fitsSystemWindows, int layoutDirection) { + if (sVerbose) { + Slog.v(TAG, "AutofillWindowPresenter.show(): fit=" + fitsSystemWindows + + ", epicenter="+ transitionEpicenter + ", dir=" + layoutDirection + + ", params=" + p); + } UiThread.getHandler().post(() -> mWindow.show(p)); } diff --git a/services/core/Android.mk b/services/core/Android.mk index f89647897d09..154934676798 100644 --- a/services/core/Android.mk +++ b/services/core/Android.mk @@ -13,7 +13,6 @@ LOCAL_SRC_FILES += \ ../../../../system/netd/server/binder/android/net/INetd.aidl \ ../../../../system/netd/server/binder/android/net/metrics/INetdEventListener.aidl \ ../../../native/cmds/installd/binder/android/os/IInstalld.aidl \ - ../../../native/services/vr/vr_window_manager/aidl/android/service/vr/IVrWindowManager.aidl \ LOCAL_AIDL_INCLUDES += \ system/netd/server/binder diff --git a/services/core/java/com/android/server/am/AppErrors.java b/services/core/java/com/android/server/am/AppErrors.java index d6bfb35b1935..cfb5478f22d4 100644 --- a/services/core/java/com/android/server/am/AppErrors.java +++ b/services/core/java/com/android/server/am/AppErrors.java @@ -740,6 +740,8 @@ class AppErrors { } // If we've created a crash dialog, show it without the lock held if(data.proc.crashDialog != null) { + Slog.i(TAG, "Showing crash dialog for package " + data.proc.info.packageName + + " u" + data.proc.userId); data.proc.crashDialog.show(); } } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 3b5e5bcce7ae..6a310f22627c 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -439,7 +439,7 @@ final class UserController { } } - Slog.d(TAG, "Sending BOOT_COMPLETE user #" + userId); + Slog.i(TAG, "Sending BOOT_COMPLETE user #" + userId); // Do not report secondary users, runtime restarts or first boot/upgrade if (userId == UserHandle.USER_SYSTEM && !mInjector.isRuntimeRestarted() && !mInjector.isFirstBootOrUpgrade()) { @@ -451,7 +451,14 @@ final class UserController { bootIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId); bootIntent.addFlags(Intent.FLAG_RECEIVER_NO_ABORT | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); - mInjector.broadcastIntentLocked(bootIntent, null, null, 0, null, null, + mInjector.broadcastIntentLocked(bootIntent, null, new IIntentReceiver.Stub() { + @Override + public void performReceive(Intent intent, int resultCode, String data, + Bundle extras, boolean ordered, boolean sticky, int sendingUser) + throws RemoteException { + Slog.i(UserController.TAG, "Finished processing BOOT_COMPLETED for u" + userId); + } + }, 0, null, null, new String[] { android.Manifest.permission.RECEIVE_BOOT_COMPLETED }, AppOpsManager.OP_NONE, null, true, false, MY_PID, SYSTEM_UID, userId); } diff --git a/services/core/java/com/android/server/display/NightDisplayService.java b/services/core/java/com/android/server/display/NightDisplayService.java index 78498967b344..d5742657c5e2 100644 --- a/services/core/java/com/android/server/display/NightDisplayService.java +++ b/services/core/java/com/android/server/display/NightDisplayService.java @@ -567,22 +567,27 @@ public final class NightDisplayService extends SystemService private final TwilightManager mTwilightManager; - private Calendar mLastActivatedTime; - TwilightAutoMode() { mTwilightManager = getLocalService(TwilightManager.class); } private void updateActivated(TwilightState state) { - boolean activate = state != null && state.isNight(); - if (state != null && mLastActivatedTime != null) { + if (state == null) { + // If there isn't a valid TwilightState then just keep the current activated + // state. + return; + } + + boolean activate = state.isNight(); + final Calendar lastActivatedTime = getLastActivatedTime(); + if (lastActivatedTime != null) { final Calendar now = Calendar.getInstance(); final Calendar sunrise = state.sunrise(); final Calendar sunset = state.sunset(); // Maintain the existing activated state if within the current period. - if (mLastActivatedTime.before(now) - && (mLastActivatedTime.after(sunrise) ^ mLastActivatedTime.after(sunset))) { + if (lastActivatedTime.before(now) + && (lastActivatedTime.after(sunrise) ^ lastActivatedTime.after(sunset))) { activate = mController.isActivated(); } } @@ -595,7 +600,6 @@ public final class NightDisplayService extends SystemService @Override public void onStart() { mTwilightManager.registerListener(this, mHandler); - mLastActivatedTime = getLastActivatedTime(); // Force an update to initialize state. updateActivated(mTwilightManager.getLastTwilightState()); @@ -604,14 +608,10 @@ public final class NightDisplayService extends SystemService @Override public void onStop() { mTwilightManager.unregisterListener(this); - mLastActivatedTime = null; } @Override public void onActivated(boolean activated) { - if (mIsActivated != null) { - mLastActivatedTime = getLastActivatedTime(); - } } @Override diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index f0ae1a735443..6aff600287cc 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -5485,7 +5485,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { // Maintain fullscreen layout until incoming animation is complete. topIsFullscreen = mTopIsFullscreen && mStatusBar.isAnimatingLw(); // Transient status bar on the lockscreen is not allowed - if (mForceStatusBarFromKeyguard && mStatusBarController.isTransientShowing()) { + if ((mForceStatusBarFromKeyguard || statusBarExpanded) + && mStatusBarController.isTransientShowing()) { mStatusBarController.updateVisibilityLw(false /*transientAllowed*/, mLastSystemUiFlags, mLastSystemUiFlags); } diff --git a/services/core/java/com/android/server/vr/VrManagerService.java b/services/core/java/com/android/server/vr/VrManagerService.java index 0e183f0a50b8..9ef74100e2ab 100644 --- a/services/core/java/com/android/server/vr/VrManagerService.java +++ b/services/core/java/com/android/server/vr/VrManagerService.java @@ -39,7 +39,6 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteCallbackList; import android.os.RemoteException; -import android.os.ServiceManager; import android.os.UserHandle; import android.provider.Settings; import android.service.notification.NotificationListenerService; @@ -47,7 +46,6 @@ import android.service.vr.IPersistentVrStateCallbacks; import android.service.vr.IVrListener; import android.service.vr.IVrManager; import android.service.vr.IVrStateCallbacks; -import android.service.vr.IVrWindowManager; import android.service.vr.VrListenerService; import android.text.TextUtils; import android.util.ArrayMap; @@ -440,18 +438,6 @@ public class VrManagerService extends SystemService implements EnabledComponentC } @Override - public void connectController(FileDescriptor fd) throws android.os.RemoteException { - enforceCallerPermission(Manifest.permission.RESTRICTED_VR_ACCESS); - VrManagerService.this.connectController(fd); - } - - @Override - public void disconnectController() throws android.os.RemoteException { - enforceCallerPermission(Manifest.permission.RESTRICTED_VR_ACCESS); - VrManagerService.this.disconnectController(); - } - - @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; @@ -1184,20 +1170,4 @@ public class VrManagerService extends SystemService implements EnabledComponentC return mVrModeEnabled; } } - - private void connectController(FileDescriptor fd) throws android.os.RemoteException { - // TODO(b/36506799): move vr_wm code to VrCore and remove this. - IVrWindowManager remote = - IVrWindowManager.Stub.asInterface( - ServiceManager.getService(IVrWindowManager.SERVICE_NAME)); - remote.connectController(fd); - } - - private void disconnectController() throws android.os.RemoteException { - // TODO(b/36506799): move vr_wm code to VrCore and remove this. - IVrWindowManager remote = - IVrWindowManager.Stub.asInterface( - ServiceManager.getService(IVrWindowManager.SERVICE_NAME)); - remote.disconnectController(); - } } diff --git a/services/core/jni/com_android_server_power_PowerManagerService.cpp b/services/core/jni/com_android_server_power_PowerManagerService.cpp index 1bdcd7aa9bd6..c722629a28f4 100644 --- a/services/core/jni/com_android_server_power_PowerManagerService.cpp +++ b/services/core/jni/com_android_server_power_PowerManagerService.cpp @@ -57,6 +57,7 @@ static struct { static jobject gPowerManagerServiceObj; sp<IPower> gPowerHal = nullptr; +bool gPowerHalExists = true; std::mutex gPowerHalMutex; static nsecs_t gLastEventTime[USER_ACTIVITY_EVENT_LAST + 1]; @@ -78,12 +79,13 @@ static bool checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodNa // Check validity of current handle to the power HAL service, and call getService() if necessary. // The caller must be holding gPowerHalMutex. bool getPowerHal() { - if (gPowerHal == nullptr) { + if (gPowerHalExists && gPowerHal == nullptr) { gPowerHal = IPower::getService(); if (gPowerHal != nullptr) { ALOGI("Loaded power HAL service"); } else { ALOGI("Couldn't load power HAL service"); + gPowerHalExists = false; } } return gPowerHal != nullptr; diff --git a/services/tests/servicestests/src/com/android/server/NightDisplayServiceTest.java b/services/tests/servicestests/src/com/android/server/NightDisplayServiceTest.java index 9a9c243023eb..58a4456ff4d7 100644 --- a/services/tests/servicestests/src/com/android/server/NightDisplayServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/NightDisplayServiceTest.java @@ -16,10 +16,12 @@ package com.android.server; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.AlarmManager; import android.content.Context; import android.content.ContextWrapper; +import android.os.Handler; import android.os.UserHandle; import android.provider.Settings; import android.provider.Settings.Secure; @@ -32,6 +34,7 @@ import com.android.internal.app.NightDisplayController.LocalTime; import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.display.DisplayTransformManager; import com.android.server.display.NightDisplayService; +import com.android.server.twilight.TwilightListener; import com.android.server.twilight.TwilightManager; import com.android.server.twilight.TwilightState; import org.junit.After; @@ -41,6 +44,10 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.Mockito.doReturn; @@ -51,7 +58,7 @@ public class NightDisplayServiceTest { private Context mContext; private int mUserId; - private TwilightManager mTwilightManager; + private MockTwilightManager mTwilightManager; private NightDisplayController mNightDisplayController; private NightDisplayService mNightDisplayService; @@ -73,7 +80,7 @@ public class NightDisplayServiceTest { final DisplayTransformManager dtm = Mockito.mock(DisplayTransformManager.class); LocalServices.addService(DisplayTransformManager.class, dtm); - mTwilightManager = Mockito.mock(TwilightManager.class); + mTwilightManager = new MockTwilightManager(); LocalServices.addService(TwilightManager.class, mTwilightManager); mNightDisplayController = new NightDisplayController(mContext, mUserId); @@ -526,23 +533,371 @@ public class NightDisplayServiceTest { assertActivated(true /* activated */); } - /** - * Convenience for making a {@link LocalTime} instance with an offset relative to now. - * - * @param offsetMinutes the offset relative to now (in minutes) - * @return the LocalTime instance - */ - private LocalTime getLocalTimeRelativeToNow(int offsetMinutes) { - final Calendar c = Calendar.getInstance(); - c.add(Calendar.MINUTE, offsetMinutes); - return new LocalTime(c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE)); + @Test + public void twilightSchedule_whenRebootedAfterNight_ifOffAfterNight_turnsOff() { + setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */); + setActivated(false /* activated */, -30 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(false /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedAfterNight_ifOffBeforeNight_turnsOff() { + setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */); + setActivated(false /* activated */, -180 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(false /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedAfterNight_ifOffDuringNight_turnsOff() { + setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */); + setActivated(false /* activated */, -90 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(false /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedAfterNight_ifOffInFuture_turnsOff() { + setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */); + setActivated(false /* activated */, 30 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(false /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedAfterNight_ifOnAfterNight_turnsOn() { + setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */); + setActivated(true /* activated */, -30 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(true /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(true /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedAfterNight_ifOnBeforeNight_turnsOff() { + setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */); + setActivated(true /* activated */, -180 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(true /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedAfterNight_ifOnDuringNight_turnsOff() { + setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */); + setActivated(true /* activated */, -90 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(true /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedAfterNight_ifOnInFuture_turnsOff() { + setAutoModeTwilight(-120 /* sunsetOffset */, -60 /* sunriseOffset */); + setActivated(true /* activated */, 30 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(true /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedBeforeNight_ifOffAfterNight_turnsOff() { + setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */); + setActivated(false /* activated */, 180 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(false /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedBeforeNight_ifOffBeforeNight_turnsOff() { + setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */); + setActivated(false /* activated */, 30 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(false /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedBeforeNight_ifOffDuringNight_turnsOff() { + setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */); + setActivated(false /* activated */, 90 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(false /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedBeforeNight_ifOffInPast_turnsOff() { + setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */); + setActivated(false /* activated */, -30 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(false /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedBeforeNight_ifOnAfterNight_turnsOff() { + setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */); + setActivated(true /* activated */, 180 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(true /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedBeforeNight_ifOnBeforeNight_turnsOff() { + setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */); + setActivated(true /* activated */, 30 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(true /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedBeforeNight_ifOnDuringNight_turnsOff() { + setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */); + setActivated(true /* activated */, 90 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(true /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedBeforeNight_ifOnInPast_turnsOn() { + setAutoModeTwilight(60 /* sunsetOffset */, 120 /* sunriseOffset */); + setActivated(true /* activated */, -30 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(true /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(true /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedDuringNight_ifOffAfterNight_turnsOn() { + setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */); + setActivated(false /* activated */, 90 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(false /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(true /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedDuringNight_ifOffBeforeNight_turnsOn() { + setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */); + setActivated(false /* activated */, -90 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(false /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(true /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedDuringNight_ifOffDuringNightInFuture_turnsOn() { + setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */); + setActivated(false /* activated */, 30 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(false /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(true /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedDuringNight_ifOffDuringNightInPast_turnsOff() { + setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */); + setActivated(false /* activated */, -30 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(false /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(false /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedDuringNight_ifOnAfterNight_turnsOn() { + setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */); + setActivated(true /* activated */, 90 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(true /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(true /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedDuringNight_ifOnBeforeNight_turnsOn() { + setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */); + setActivated(true /* activated */, -90 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(true /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(true /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedDuringNight_ifOnDuringNightInFuture_turnsOn() { + setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */); + setActivated(true /* activated */, 30 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(true /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(true /* activated */); + } + + @Test + public void twilightSchedule_whenRebootedDuringNight_ifOnDuringNightInPast_turnsOn() { + setAutoModeTwilight(-60 /* sunsetOffset */, 60 /* sunriseOffset */); + setActivated(true /* activated */, -30 /* lastActivatedTimeOffset */); + + final TwilightState state = mTwilightManager.getLastTwilightState(); + mTwilightManager.setTwilightState(null); + + startService(); + assertActivated(true /* activated */); + + mTwilightManager.setTwilightState(state); + assertActivated(true /* activated */); } /** * Configures Night display to use a custom schedule. * * @param startTimeOffset the offset relative to now to activate Night display (in minutes) - * @param endTimeOffset the offset relative to now to deactivate Night display (in minutes) + * @param endTimeOffset the offset relative to now to deactivate Night display (in minutes) */ private void setAutoModeCustom(int startTimeOffset, int endTimeOffset) { mNightDisplayController.setAutoMode(NightDisplayController.AUTO_MODE_CUSTOM); @@ -553,34 +908,21 @@ public class NightDisplayServiceTest { /** * Configures Night display to use the twilight schedule. * - * @param sunsetOffset the offset relative to now for sunset (in minutes) + * @param sunsetOffset the offset relative to now for sunset (in minutes) * @param sunriseOffset the offset relative to now for sunrise (in minutes) */ private void setAutoModeTwilight(int sunsetOffset, int sunriseOffset) { mNightDisplayController.setAutoMode(NightDisplayController.AUTO_MODE_TWILIGHT); - - final LocalTime sunset = getLocalTimeRelativeToNow(sunsetOffset); - final LocalTime sunrise = getLocalTimeRelativeToNow(sunriseOffset); - - final Calendar now = Calendar.getInstance(); - long sunsetMillis = sunset.getDateTimeBefore(now).getTimeInMillis(); - long sunriseMillis = sunrise.getDateTimeBefore(now).getTimeInMillis(); - if (sunsetMillis < sunriseMillis) { - sunsetMillis = sunset.getDateTimeAfter(now).getTimeInMillis(); - } else { - sunriseMillis = sunrise.getDateTimeAfter(now).getTimeInMillis(); - } - - final TwilightState state = new TwilightState(sunriseMillis, sunsetMillis); - doReturn(state).when(mTwilightManager).getLastTwilightState(); + mTwilightManager.setTwilightState( + getTwilightStateRelativeToNow(sunsetOffset, sunriseOffset)); } /** * Configures the Night display activated state. * - * @param activated {@code true} if Night display should be activated + * @param activated {@code true} if Night display should be activated * @param lastActivatedTimeOffset the offset relative to now to record that Night display was - activated (in minutes) + * activated (in minutes) */ private void setActivated(boolean activated, int lastActivatedTimeOffset) { mNightDisplayController.setActivated(activated); @@ -617,4 +959,93 @@ public class NightDisplayServiceTest { .that(mNightDisplayController.isActivated()) .isEqualTo(activated); } + + /** + * Convenience for making a {@link LocalTime} instance with an offset relative to now. + * + * @param offsetMinutes the offset relative to now (in minutes) + * @return the LocalTime instance + */ + private static LocalTime getLocalTimeRelativeToNow(int offsetMinutes) { + final Calendar c = Calendar.getInstance(); + c.add(Calendar.MINUTE, offsetMinutes); + return new LocalTime(c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE)); + } + + /** + * Convenience for making a {@link TwilightState} instance with sunrise/sunset relative to now. + * + * @param sunsetOffset the offset relative to now for sunset (in minutes) + * @param sunriseOffset the offset relative to now for sunrise (in minutes) + * @return the TwilightState instance + */ + private static TwilightState getTwilightStateRelativeToNow(int sunsetOffset, + int sunriseOffset) { + final LocalTime sunset = getLocalTimeRelativeToNow(sunsetOffset); + final LocalTime sunrise = getLocalTimeRelativeToNow(sunriseOffset); + + final Calendar now = Calendar.getInstance(); + long sunsetMillis = sunset.getDateTimeBefore(now).getTimeInMillis(); + long sunriseMillis = sunrise.getDateTimeBefore(now).getTimeInMillis(); + if (sunsetMillis < sunriseMillis) { + sunsetMillis = sunset.getDateTimeAfter(now).getTimeInMillis(); + } else { + sunriseMillis = sunrise.getDateTimeAfter(now).getTimeInMillis(); + } + + return new TwilightState(sunriseMillis, sunsetMillis); + } + + private static class MockTwilightManager implements TwilightManager { + + private final Map<TwilightListener, Handler> mListeners = new HashMap<>(); + private TwilightState mTwilightState; + + /** + * Updates the TwilightState and notifies any registered listeners. + * + * @param state the new TwilightState to use + */ + void setTwilightState(TwilightState state) { + synchronized (mListeners) { + mTwilightState = state; + + final CountDownLatch latch = new CountDownLatch(mListeners.size()); + for (Map.Entry<TwilightListener, Handler> entry : mListeners.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onTwilightStateChanged(state); + latch.countDown(); + } + }); + } + + try { + latch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void registerListener(@NonNull TwilightListener listener, @NonNull Handler handler) { + synchronized (mListeners) { + mListeners.put(listener, handler); + } + } + + @Override + public void unregisterListener(@NonNull TwilightListener listener) { + synchronized (mListeners) { + mListeners.remove(listener); + } + } + + @Override + public TwilightState getLastTwilightState() { + return mTwilightState; + } + } } |