diff options
174 files changed, 7616 insertions, 1479 deletions
diff --git a/apex/media/framework/java/android/media/MediaParser.java b/apex/media/framework/java/android/media/MediaParser.java index d1e9e4e4289b..50f4ddd5e2e3 100644 --- a/apex/media/framework/java/android/media/MediaParser.java +++ b/apex/media/framework/java/android/media/MediaParser.java @@ -1412,14 +1412,12 @@ public final class MediaParser { setOptionalMediaFormatInt(result, MediaFormat.KEY_HEIGHT, format.height); List<byte[]> initData = format.initializationData; - if (initData != null) { - for (int i = 0; i < initData.size(); i++) { - result.setByteBuffer("csd-" + i, ByteBuffer.wrap(initData.get(i))); - } + for (int i = 0; i < initData.size(); i++) { + result.setByteBuffer("csd-" + i, ByteBuffer.wrap(initData.get(i))); } + setPcmEncoding(format, result); setOptionalMediaFormatString(result, MediaFormat.KEY_LANGUAGE, format.language); setOptionalMediaFormatInt(result, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); - setOptionalMediaFormatInt(result, MediaFormat.KEY_PCM_ENCODING, format.pcmEncoding); setOptionalMediaFormatInt(result, MediaFormat.KEY_ROTATION, format.rotationDegrees); setOptionalMediaFormatInt(result, MediaFormat.KEY_SAMPLE_RATE, format.sampleRate); setOptionalMediaFormatInt( @@ -1462,6 +1460,27 @@ public final class MediaParser { return result; } + private static void setPcmEncoding(Format format, MediaFormat result) { + int exoPcmEncoding = format.pcmEncoding; + setOptionalMediaFormatInt(result, "exo-pcm-encoding", format.pcmEncoding); + int mediaFormatPcmEncoding; + switch (exoPcmEncoding) { + case C.ENCODING_PCM_8BIT: + mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_8BIT; + break; + case C.ENCODING_PCM_16BIT: + mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_16BIT; + break; + case C.ENCODING_PCM_FLOAT: + mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_FLOAT; + break; + default: + // No matching value. Do nothing. + return; + } + result.setInteger(MediaFormat.KEY_PCM_ENCODING, mediaFormatPcmEncoding); + } + private static void setOptionalMediaFormatInt(MediaFormat mediaFormat, String key, int value) { if (value != Format.NO_VALUE) { mediaFormat.setInteger(key, value); diff --git a/apex/sdkextensions/framework/java/android/os/ext/SdkExtensions.java b/apex/sdkextensions/framework/java/android/os/ext/SdkExtensions.java index 103b53e81db5..c268ff4291e4 100644 --- a/apex/sdkextensions/framework/java/android/os/ext/SdkExtensions.java +++ b/apex/sdkextensions/framework/java/android/os/ext/SdkExtensions.java @@ -62,7 +62,11 @@ public class SdkExtensions { if (sdk < VERSION_CODES.R) { throw new IllegalArgumentException(String.valueOf(sdk) + " does not have extensions"); } - return R_EXTENSION_INT; + + if (sdk == VERSION_CODES.R) { + return R_EXTENSION_INT; + } + return 0; } } diff --git a/cmds/screencap/screencap.cpp b/cmds/screencap/screencap.cpp index 4410f1c4570c..bb32dd2fa7ad 100644 --- a/cmds/screencap/screencap.cpp +++ b/cmds/screencap/screencap.cpp @@ -105,7 +105,7 @@ static status_t notifyMediaScanner(const char* fileName) { char *cmd[] = { (char*) "am", (char*) "broadcast", - (char*) "am", + (char*) "-a", (char*) "android.intent.action.MEDIA_SCANNER_SCAN_FILE", (char*) "-d", &filePath[0], diff --git a/cmds/statsd/src/main.cpp b/cmds/statsd/src/main.cpp index e3945334aeca..cd9c4e5b947b 100644 --- a/cmds/statsd/src/main.cpp +++ b/cmds/statsd/src/main.cpp @@ -37,6 +37,7 @@ using std::shared_ptr; using std::make_shared; shared_ptr<StatsService> gStatsService = nullptr; +sp<StatsSocketListener> gSocketListener = nullptr; void signalHandler(int sig) { if (sig == SIGPIPE) { @@ -47,6 +48,7 @@ void signalHandler(int sig) { return; } + if (gSocketListener != nullptr) gSocketListener->stopListener(); if (gStatsService != nullptr) gStatsService->Terminate(); ALOGW("statsd terminated on receiving signal %d.", sig); exit(1); @@ -92,11 +94,11 @@ int main(int /*argc*/, char** /*argv*/) { gStatsService->Startup(); - sp<StatsSocketListener> socketListener = new StatsSocketListener(eventQueue); + gSocketListener = new StatsSocketListener(eventQueue); ALOGI("Statsd starts to listen to socket."); // Backlog and /proc/sys/net/unix/max_dgram_qlen set to large value - if (socketListener->startListener(600)) { + if (gSocketListener->startListener(600)) { exit(1); } diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 67334f554df7..18e5c3d2d7cc 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -494,6 +494,13 @@ public abstract class AccessibilityService extends Service { */ public static final int GLOBAL_ACTION_TAKE_SCREENSHOT = 9; + /** + * Action to send the KEYCODE_HEADSETHOOK KeyEvent, which is used to answer/hang up calls and + * play/stop media + * @hide + */ + public static final int GLOBAL_ACTION_KEYCODE_HEADSETHOOK = 10; + private static final String LOG_TAG = "AccessibilityService"; /** diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 8e0d939487e3..37f683ef435f 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -233,12 +233,9 @@ public class AppOpsManager { } private void reportStackTraceIfNeeded(@NonNull SyncNotedAppOp op) { - if (sConfig.getSampledOpCode() == OP_NONE - && sConfig.getExpirationTimeSinceBootMillis() - >= SystemClock.elapsedRealtime()) { + if (!isCollectingStackTraces()) { return; } - MessageSamplingConfig config = sConfig; if (leftCircularDistance(strOpToOp(op.getOp()), config.getSampledOpCode(), _NUM_OP) <= config.getAcceptableLeftDistance() @@ -8181,7 +8178,22 @@ public class AppOpsManager { * @hide */ public static boolean isListeningForOpNoted() { - return sOnOpNotedCallback != null; + return sOnOpNotedCallback != null || isCollectingStackTraces(); + } + + /** + * @return {@code true} iff the process is currently sampled for stacktrace collection. + * + * @see #setOnOpNotedCallback + * + * @hide + */ + private static boolean isCollectingStackTraces() { + if (sConfig.getSampledOpCode() == OP_NONE && + sConfig.getExpirationTimeSinceBootMillis() >= SystemClock.elapsedRealtime()) { + return false; + } + return true; } /** diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl index 1211c731e2e1..ed6ba0c5efa4 100644 --- a/core/java/android/app/usage/IUsageStatsManager.aidl +++ b/core/java/android/app/usage/IUsageStatsManager.aidl @@ -43,7 +43,7 @@ interface IUsageStatsManager { @UnsupportedAppUsage void setAppInactive(String packageName, boolean inactive, int userId); @UnsupportedAppUsage - boolean isAppInactive(String packageName, int userId); + boolean isAppInactive(String packageName, int userId, String callingPackage); void onCarrierPrivilegedAppsChanged(); void reportChooserSelection(String packageName, int userId, String contentType, in String[] annotations, String action); diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java index 0a67802fe92b..2ce6a86753a6 100644 --- a/core/java/android/app/usage/UsageStatsManager.java +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -622,12 +622,17 @@ public final class UsageStatsManager { * app hasn't been used directly or indirectly for a period of time defined by the system. This * could be of the order of several hours or days. Apps are not considered inactive when the * device is charging. + * <p> The caller must have {@link android.Manifest.permission#PACKAGE_USAGE_STATS} to query the + * inactive state of other apps</p> + * * @param packageName The package name of the app to query - * @return whether the app is currently considered inactive + * @return whether the app is currently considered inactive or false if querying another app + * without {@link android.Manifest.permission#PACKAGE_USAGE_STATS} */ public boolean isAppInactive(String packageName) { try { - return mService.isAppInactive(packageName, mContext.getUserId()); + return mService.isAppInactive(packageName, mContext.getUserId(), + mContext.getOpPackageName()); } catch (RemoteException e) { // fall through and return default } diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 1dadbda1918b..85bafd9d37e2 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -1695,7 +1695,7 @@ public class PackageParser { } // Check to see if overlay should be excluded based on system property condition - if (!checkRequiredSystemProperty(requiredSystemPropertyName, + if (!checkRequiredSystemProperties(requiredSystemPropertyName, requiredSystemPropertyValue)) { Slog.i(TAG, "Skipping target and overlay pair " + targetPackage + " and " + codePath + ": overlay ignored due to required system property: " @@ -1997,7 +1997,7 @@ public class PackageParser { } // check to see if overlay should be excluded based on system property condition - if (!checkRequiredSystemProperty(propName, propValue)) { + if (!checkRequiredSystemProperties(propName, propValue)) { Slog.i(TAG, "Skipping target and overlay pair " + pkg.mOverlayTarget + " and " + pkg.baseCodePath+ ": overlay ignored due to required system property: " + propName + " with value: " + propValue); @@ -2427,24 +2427,42 @@ public class PackageParser { /** * Returns {@code true} if both the property name and value are empty or if the given system - * property is set to the specified value. In all other cases, returns {@code false} + * property is set to the specified value. Properties can be one or more, and if properties are + * more than one, they must be separated by comma, and count of names and values must be equal, + * and also every given system property must be set to the corresponding value. + * In all other cases, returns {@code false} */ - public static boolean checkRequiredSystemProperty(String propName, String propValue) { - if (TextUtils.isEmpty(propName) || TextUtils.isEmpty(propValue)) { - if (!TextUtils.isEmpty(propName) || !TextUtils.isEmpty(propValue)) { + public static boolean checkRequiredSystemProperties(@Nullable String rawPropNames, + @Nullable String rawPropValues) { + if (TextUtils.isEmpty(rawPropNames) || TextUtils.isEmpty(rawPropValues)) { + if (!TextUtils.isEmpty(rawPropNames) || !TextUtils.isEmpty(rawPropValues)) { // malformed condition - incomplete - Slog.w(TAG, "Disabling overlay - incomplete property :'" + propName - + "=" + propValue + "' - require both requiredSystemPropertyName" - + " AND requiredSystemPropertyValue to be specified."); + Slog.w(TAG, "Disabling overlay - incomplete property :'" + rawPropNames + + "=" + rawPropValues + "' - require both requiredSystemPropertyName" + + " AND requiredSystemPropertyValue to be specified."); return false; } // no valid condition set - so no exclusion criteria, overlay will be included. return true; } - // check property value - make sure it is both set and equal to expected value - final String currValue = SystemProperties.get(propName); - return (currValue != null && currValue.equals(propValue)); + final String[] propNames = rawPropNames.split(","); + final String[] propValues = rawPropValues.split(","); + + if (propNames.length != propValues.length) { + Slog.w(TAG, "Disabling overlay - property :'" + rawPropNames + + "=" + rawPropValues + "' - require both requiredSystemPropertyName" + + " AND requiredSystemPropertyValue lists to have the same size."); + return false; + } + for (int i = 0; i < propNames.length; i++) { + // Check property value: make sure it is both set and equal to expected value + final String currValue = SystemProperties.get(propNames[i]); + if (!TextUtils.equals(currValue, propValues[i])) { + return false; + } + } + return true; } /** diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index 27399e4b39bc..2f416a2538ba 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -413,7 +413,7 @@ public class ApkLiteParseUtils { } // Check to see if overlay should be excluded based on system property condition - if (!PackageParser.checkRequiredSystemProperty(requiredSystemPropertyName, + if (!PackageParser.checkRequiredSystemProperties(requiredSystemPropertyName, requiredSystemPropertyValue)) { Slog.i(TAG, "Skipping target and overlay pair " + targetPackage + " and " + codePath + ": overlay ignored due to required system property: " diff --git a/core/java/android/content/pm/parsing/ParsingPackageUtils.java b/core/java/android/content/pm/parsing/ParsingPackageUtils.java index 29ece4924e5c..88f4c31b82cc 100644 --- a/core/java/android/content/pm/parsing/ParsingPackageUtils.java +++ b/core/java/android/content/pm/parsing/ParsingPackageUtils.java @@ -84,7 +84,6 @@ import android.os.Build; import android.os.Bundle; import android.os.FileUtils; import android.os.RemoteException; -import android.os.SystemProperties; import android.os.Trace; import android.os.ext.SdkExtensions; import android.text.TextUtils; @@ -2348,7 +2347,7 @@ public class ParsingPackageUtils { R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyName); String propValue = sa.getString( R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyValue); - if (!checkOverlayRequiredSystemProperty(propName, propValue)) { + if (!PackageParser.checkRequiredSystemProperties(propName, propValue)) { Slog.i(TAG, "Skipping target and overlay pair " + target + " and " + pkg.getBaseCodePath() + ": overlay ignored due to required system property: " @@ -2522,24 +2521,6 @@ public class ParsingPackageUtils { } } - private static boolean checkOverlayRequiredSystemProperty(String propName, String propValue) { - if (TextUtils.isEmpty(propName) || TextUtils.isEmpty(propValue)) { - if (!TextUtils.isEmpty(propName) || !TextUtils.isEmpty(propValue)) { - // malformed condition - incomplete - Slog.w(TAG, "Disabling overlay - incomplete property :'" + propName - + "=" + propValue + "' - require both requiredSystemPropertyName" - + " AND requiredSystemPropertyValue to be specified."); - return false; - } - // no valid condition set - so no exclusion criteria, overlay will be included. - return true; - } - - // check property value - make sure it is both set and equal to expected value - final String currValue = SystemProperties.get(propName); - return (currValue != null && currValue.equals(propValue)); - } - /** * This is a pre-density application which will get scaled - instead of being pixel perfect. * This type of application is not resizable. diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index ca861577ab37..1fef07156310 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -897,6 +897,34 @@ public abstract class VibrationEffect implements Parcelable { return -1; } + /** + * Scale all primitives of this effect. + * + * @param gamma the gamma adjustment to apply + * @param maxAmplitude the new maximum amplitude of the effect, must be between 0 and + * MAX_AMPLITUDE + * @throws IllegalArgumentException if maxAmplitude less than 0 or more than MAX_AMPLITUDE + * + * @return A {@link Composed} effect with same but scaled primitives. + */ + public Composed scale(float gamma, int maxAmplitude) { + if (maxAmplitude > MAX_AMPLITUDE || maxAmplitude < 0) { + throw new IllegalArgumentException( + "Amplitude is negative or greater than MAX_AMPLITUDE"); + } + if (gamma == 1.0f && maxAmplitude == MAX_AMPLITUDE) { + // Just return a copy of the original if there's no scaling to be done. + return new Composed(mPrimitiveEffects); + } + List<Composition.PrimitiveEffect> scaledPrimitives = new ArrayList<>(); + for (Composition.PrimitiveEffect primitive : mPrimitiveEffects) { + float adjustedScale = MathUtils.pow(primitive.scale, gamma); + float newScale = adjustedScale * maxAmplitude / (float) MAX_AMPLITUDE; + scaledPrimitives.add(new Composition.PrimitiveEffect( + primitive.id, newScale, primitive.delay)); + } + return new Composed(scaledPrimitives); + } /** * @hide diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index af915966e7eb..1f6555c85a66 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -102,8 +102,8 @@ public class ZenModeConfig implements Parcelable { private static final boolean DEFAULT_ALLOW_REMINDERS = false; private static final boolean DEFAULT_ALLOW_EVENTS = false; private static final boolean DEFAULT_ALLOW_REPEAT_CALLERS = true; - private static final boolean DEFAULT_ALLOW_CONV = true; - private static final int DEFAULT_ALLOW_CONV_FROM = ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; + private static final boolean DEFAULT_ALLOW_CONV = false; + private static final int DEFAULT_ALLOW_CONV_FROM = ZenPolicy.CONVERSATION_SENDERS_NONE; private static final boolean DEFAULT_CHANNELS_BYPASSING_DND = false; private static final int DEFAULT_SUPPRESSED_VISUAL_EFFECTS = 0; @@ -125,8 +125,8 @@ public class ZenModeConfig implements Parcelable { private static final String ALLOW_ATT_EVENTS = "events"; private static final String ALLOW_ATT_SCREEN_OFF = "visualScreenOff"; private static final String ALLOW_ATT_SCREEN_ON = "visualScreenOn"; - private static final String ALLOW_ATT_CONV = "conv"; - private static final String ALLOW_ATT_CONV_FROM = "convFrom"; + private static final String ALLOW_ATT_CONV = "convos"; + private static final String ALLOW_ATT_CONV_FROM = "convosFrom"; private static final String DISALLOW_TAG = "disallow"; private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects"; private static final String STATE_TAG = "state"; diff --git a/core/java/android/view/IScrollCaptureClient.aidl b/core/java/android/view/IScrollCaptureClient.aidl new file mode 100644 index 000000000000..5f135a37dfef --- /dev/null +++ b/core/java/android/view/IScrollCaptureClient.aidl @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.graphics.Rect; +import android.view.Surface; + + + /** + * Interface implemented by a client of the Scroll Capture framework to receive requests + * to start, capture images and end the session. + * + * {@hide} + */ +interface IScrollCaptureClient { + + /** + * Informs the client that it has been selected for scroll capture and should prepare to + * to begin handling capture requests. + */ + oneway void startCapture(in Surface surface); + + /** + * Request the client capture an image within the provided rectangle. + * + * @see android.view.ScrollCaptureCallback#onScrollCaptureRequest + */ + oneway void requestImage(in Rect captureArea); + + /** + * Inform the client that capture has ended. The client should shut down and release all + * local resources in use and prepare for return to normal interactive usage. + */ + oneway void endCapture(); +} diff --git a/core/java/android/view/IScrollCaptureController.aidl b/core/java/android/view/IScrollCaptureController.aidl new file mode 100644 index 000000000000..8474a00b302f --- /dev/null +++ b/core/java/android/view/IScrollCaptureController.aidl @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.graphics.Point; +import android.graphics.Rect; +import android.view.Surface; + +import android.view.IScrollCaptureClient; + +/** + * Interface to a controller passed to the {@link IScrollCaptureClient} which provides the client an + * asynchronous callback channel for responses. + * + * {@hide} + */ +interface IScrollCaptureController { + /** + * Scroll capture is available, and a client connect has been returned. + * + * @param client interface to a ScrollCaptureCallback in the window process + * @param scrollAreaInWindow the location of scrolling in global (window) coordinate space + */ + oneway void onClientConnected(in IScrollCaptureClient client, in Rect scrollBounds, + in Point positionInWindow); + + /** + * Nothing in the window can be scrolled, scroll capture not offered. + */ + oneway void onClientUnavailable(); + + /** + * Notifies the system that the client has confirmed the request and is ready to begin providing + * image requests. + */ + oneway void onCaptureStarted(); + + /** + * Received a response from a capture request. + */ + oneway void onCaptureBufferSent(long frameNumber, in Rect capturedArea); + + /** + * Signals that the capture session has completed and the target window may be returned to + * normal interactive use. This may be due to normal shutdown, or after a timeout or other + * unrecoverable state change such as activity lifecycle, window visibility or focus. + */ + oneway void onConnectionClosed(); +} diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl index 9f2d36de61cd..e09bf9d2e80a 100644 --- a/core/java/android/view/IWindow.aidl +++ b/core/java/android/view/IWindow.aidl @@ -21,15 +21,16 @@ import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; import android.os.ParcelFileDescriptor; +import android.util.MergedConfiguration; +import android.view.DisplayCutout; import android.view.DragEvent; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.IScrollCaptureController; import android.view.KeyEvent; import android.view.MotionEvent; -import android.view.DisplayCutout; -import android.view.InsetsState; -import android.view.InsetsSourceControl; import com.android.internal.os.IResultReceiver; -import android.util.MergedConfiguration; /** * API back to a client window that the Window Manager uses to inform it of @@ -139,4 +140,11 @@ oneway interface IWindow { * Tell the window that it is either gaining or losing pointer capture. */ void dispatchPointerCaptureChanged(boolean hasCapture); + + /** + * Called when Scroll Capture support is requested for a window. + * + * @param controller the controller to receive responses + */ + void requestScrollCapture(in IScrollCaptureController controller); } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index b0bacb955f80..b3b53f029382 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -42,6 +42,7 @@ import android.view.IDisplayFoldListener; import android.view.IDisplayWindowRotationController; import android.view.IOnKeyguardExitResult; import android.view.IPinnedStackListener; +import android.view.IScrollCaptureController; import android.view.RemoteAnimationAdapter; import android.view.IRotationWatcher; import android.view.ISystemGestureExclusionListener; @@ -749,4 +750,18 @@ interface IWindowManager * @param flags see definition in SurfaceTracing.cpp */ void setLayerTracingFlags(int flags); + + /** + * Forwards a scroll capture request to the appropriate window, if available. + * + * @param displayId the id of the display to target + * @param behindClient token for a window, used to filter the search to windows behind it, or + * {@code null} to accept a window at any zOrder + * @param taskId specifies the id of a task the result must belong to, or -1 to ignore task ids + * @param controller the controller to receive results, a call to either + * {@link IScrollCaptureController#onClientConnected} or + * {@link IScrollCaptureController#onClientUnavailable}. + */ + void requestScrollCapture(int displayId, IBinder behindClient, int taskId, + IScrollCaptureController controller); } diff --git a/core/java/android/view/ScrollCaptureCallback.java b/core/java/android/view/ScrollCaptureCallback.java new file mode 100644 index 000000000000..e1a4e7443600 --- /dev/null +++ b/core/java/android/view/ScrollCaptureCallback.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.NonNull; +import android.annotation.UiThread; +import android.graphics.Rect; + +import java.util.function.Consumer; + +/** + * A ScrollCaptureCallback is responsible for providing rendered snapshots of scrolling content for + * the scroll capture system. A single callback is responsible for providing support to a single + * scrolling UI element. At request time, the system will select the best candidate from among all + * callbacks registered within the window. + * <p> + * A callback is assigned to a View using {@link View#setScrollCaptureCallback}, or to the window as + * {@link Window#addScrollCaptureCallback}. The point where the callback is registered defines the + * frame of reference for the bounds measurements used. + * <p> + * <b>Terminology</b> + * <dl> + * <dt>Containing View</dt> + * <dd>The view on which this callback is attached, or the root view of the window if the callback + * is assigned directly to a window.</dd> + * + * <dt>Scroll Bounds</dt> + * <dd>A rectangle which describes an area within the containing view where scrolling content may + * be positioned. This may be the Containing View bounds itself, or any rectangle within. + * Requested by {@link #onScrollCaptureSearch}.</dd> + * + * <dt>Scroll Delta</dt> + * <dd>The distance the scroll position has moved since capture started. Implementations are + * responsible for tracking changes in vertical scroll position during capture. This is required to + * map the capture area to the correct location, given the current scroll position. + * + * <dt>Capture Area</dt> + * <dd>A rectangle which describes the area to capture, relative to scroll bounds. The vertical + * position remains relative to the starting scroll position and any movement since ("Scroll Delta") + * should be subtracted to locate the correct local position, and scrolled into view as necessary. + * </dd> + * </dl> + * + * @see View#setScrollCaptureHint(int) + * @see View#setScrollCaptureCallback(ScrollCaptureCallback) + * @see Window#addScrollCaptureCallback(ScrollCaptureCallback) + * + * @hide + */ +@UiThread +public interface ScrollCaptureCallback { + + /** + * The system is searching for the appropriate scrolling container to capture and would like to + * know the size and position of scrolling content handled by this callback. + * <p> + * Implementations should inset {@code containingViewBounds} to cover only the area within the + * containing view where scrolling content may be positioned. This should cover only the content + * which tracks with scrolling movement. + * <p> + * Return the updated rectangle to {@code resultConsumer}. If for any reason the scrolling + * content is not available to capture, a {@code null} rectangle may be returned, and this view + * will be excluded as the target for this request. + * <p> + * Responses received after XXXms will be discarded. + * <p> + * TODO: finalize timeout + * + * @param onReady consumer for the updated rectangle + */ + void onScrollCaptureSearch(@NonNull Consumer<Rect> onReady); + + /** + * Scroll Capture has selected this callback to provide the scrolling image content. + * <p> + * The onReady signal should be called when ready to begin handling image requests. + */ + void onScrollCaptureStart(@NonNull ScrollCaptureSession session, @NonNull Runnable onReady); + + /** + * An image capture has been requested from the scrolling content. + * <p> + * <code>captureArea</code> contains the bounds of the image requested, relative to the + * rectangle provided by {@link ScrollCaptureCallback#onScrollCaptureSearch}, referred to as + * {@code scrollBounds}. + * here. + * <p> + * A series of requests will step by a constant vertical amount relative to {@code + * scrollBounds}, moving through the scrolling range of content, above and below the current + * visible area. The rectangle's vertical position will not account for any scrolling movement + * since capture started. Implementations therefore must track any scroll position changes and + * subtract this distance from requests. + * <p> + * To handle a request, the content should be scrolled to maximize the visible area of the + * requested rectangle. Offset {@code captureArea} again to account for any further scrolling. + * <p> + * Finally, clip this rectangle against scrollBounds to determine what portion, if any is + * visible content to capture. If the rectangle is completely clipped, set it to {@link + * Rect#setEmpty() empty} and skip the next step. + * <p> + * Make a copy of {@code captureArea}, transform to window coordinates and draw the window, + * clipped to this rectangle, into the {@link ScrollCaptureSession#getSurface() surface} at + * offset (0,0). + * <p> + * Finally, return the resulting {@code captureArea} using + * {@link ScrollCaptureSession#notifyBufferSent}. + * <p> + * If the response is not supplied within XXXms, the session will end with a call to {@link + * #onScrollCaptureEnd}, after which {@code session} is invalid and should be discarded. + * <p> + * TODO: finalize timeout + * <p> + * + * @param captureArea the area to capture, a rectangle within {@code scrollBounds} + */ + void onScrollCaptureImageRequest( + @NonNull ScrollCaptureSession session, @NonNull Rect captureArea); + + /** + * Signals that capture has ended. Implementations should release any temporary resources or + * references to objects in use during the capture. Any resources obtained from the session are + * now invalid and attempts to use them after this point may throw an exception. + * <p> + * The window should be returned as much as possible to its original state when capture started. + * At a minimum, the content should be scrolled to its original position. + * <p> + * <code>onReady</code> should be called when the window should be made visible and + * interactive. The system will wait up to XXXms for this call before proceeding. + * <p> + * TODO: finalize timeout + * + * @param onReady a callback to inform the system that the application has completed any + * cleanup and is ready to become visible + */ + void onScrollCaptureEnd(@NonNull Runnable onReady); +} + diff --git a/core/java/android/view/ScrollCaptureClient.java b/core/java/android/view/ScrollCaptureClient.java new file mode 100644 index 000000000000..f163124f3a98 --- /dev/null +++ b/core/java/android/view/ScrollCaptureClient.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UiThread; +import android.annotation.WorkerThread; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; +import android.os.RemoteException; +import android.util.CloseGuard; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A client of the system providing Scroll Capture capability on behalf of a Window. + * <p> + * An instance is created to wrap the selected {@link ScrollCaptureCallback}. + * + * @hide + */ +public class ScrollCaptureClient extends IScrollCaptureClient.Stub { + + private static final String TAG = "ScrollCaptureClient"; + private static final int DEFAULT_TIMEOUT = 1000; + + private final Handler mHandler; + private ScrollCaptureTarget mSelectedTarget; + private int mTimeoutMillis = DEFAULT_TIMEOUT; + + protected Surface mSurface; + private IScrollCaptureController mController; + + private final Rect mScrollBounds; + private final Point mPositionInWindow; + private final CloseGuard mCloseGuard; + + // The current session instance in use by the callback. + private ScrollCaptureSession mSession; + + // Helps manage timeout callbacks registered to handler and aids testing. + private DelayedAction mTimeoutAction; + + /** + * Constructs a ScrollCaptureClient. + * + * @param selectedTarget the target the client is controlling + * @param controller the callbacks to reply to system requests + * + * @hide + */ + public ScrollCaptureClient( + @NonNull ScrollCaptureTarget selectedTarget, + @NonNull IScrollCaptureController controller) { + requireNonNull(selectedTarget, "<selectedTarget> must non-null"); + requireNonNull(controller, "<controller> must non-null"); + final Rect scrollBounds = requireNonNull(selectedTarget.getScrollBounds(), + "target.getScrollBounds() must be non-null to construct a client"); + + mSelectedTarget = selectedTarget; + mHandler = selectedTarget.getContainingView().getHandler(); + mScrollBounds = new Rect(scrollBounds); + mPositionInWindow = new Point(selectedTarget.getPositionInWindow()); + + mController = controller; + mCloseGuard = new CloseGuard(); + mCloseGuard.open("close"); + + selectedTarget.getContainingView().addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + + } + + @Override + public void onViewDetachedFromWindow(View v) { + selectedTarget.getContainingView().removeOnAttachStateChangeListener(this); + endCapture(); + } + }); + } + + @VisibleForTesting + public void setTimeoutMillis(int timeoutMillis) { + mTimeoutMillis = timeoutMillis; + } + + @Nullable + @VisibleForTesting + public DelayedAction getTimeoutAction() { + return mTimeoutAction; + } + + private void checkConnected() { + if (mSelectedTarget == null || mController == null) { + throw new IllegalStateException("This client has been disconnected."); + } + } + + private void checkStarted() { + if (mSession == null) { + throw new IllegalStateException("Capture session has not been started!"); + } + } + + @WorkerThread // IScrollCaptureClient + @Override + public void startCapture(Surface surface) throws RemoteException { + checkConnected(); + mSurface = surface; + scheduleTimeout(mTimeoutMillis, this::onStartCaptureTimeout); + mSession = new ScrollCaptureSession(mSurface, mScrollBounds, mPositionInWindow, this); + mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureStart(mSession, + this::onStartCaptureCompleted)); + } + + @UiThread + private void onStartCaptureCompleted() { + if (cancelTimeout()) { + mHandler.post(() -> { + try { + mController.onCaptureStarted(); + } catch (RemoteException e) { + doShutdown(); + } + }); + } + } + + @UiThread + private void onStartCaptureTimeout() { + endCapture(); + } + + @WorkerThread // IScrollCaptureClient + @Override + public void requestImage(Rect requestRect) { + checkConnected(); + checkStarted(); + scheduleTimeout(mTimeoutMillis, this::onRequestImageTimeout); + // Response is dispatched via ScrollCaptureSession, to onRequestImageCompleted + mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureImageRequest( + mSession, new Rect(requestRect))); + } + + @UiThread + void onRequestImageCompleted(long frameNumber, Rect capturedArea) { + final Rect finalCapturedArea = new Rect(capturedArea); + if (cancelTimeout()) { + mHandler.post(() -> { + try { + mController.onCaptureBufferSent(frameNumber, finalCapturedArea); + } catch (RemoteException e) { + doShutdown(); + } + }); + } + } + + @UiThread + private void onRequestImageTimeout() { + endCapture(); + } + + @WorkerThread // IScrollCaptureClient + @Override + public void endCapture() { + if (isStarted()) { + scheduleTimeout(mTimeoutMillis, this::onEndCaptureTimeout); + mHandler.post(() -> + mSelectedTarget.getCallback().onScrollCaptureEnd(this::onEndCaptureCompleted)); + } else { + disconnect(); + } + } + + private boolean isStarted() { + return mController != null && mSelectedTarget != null; + } + + @UiThread + private void onEndCaptureCompleted() { // onEndCaptureCompleted + if (cancelTimeout()) { + doShutdown(); + } + } + + @UiThread + private void onEndCaptureTimeout() { + doShutdown(); + } + + + private void doShutdown() { + try { + if (mController != null) { + mController.onConnectionClosed(); + } + } catch (RemoteException e) { + // Ignore + } finally { + disconnect(); + } + } + + /** + * Shuts down this client and releases references to dependent objects. No attempt is made + * to notify the controller, use with caution! + */ + public void disconnect() { + if (mSession != null) { + mSession.disconnect(); + mSession = null; + } + + mSelectedTarget = null; + mController = null; + } + + /** @return a string representation of the state of this client */ + public String toString() { + return "ScrollCaptureClient{" + + ", session=" + mSession + + ", selectedTarget=" + mSelectedTarget + + ", clientCallbacks=" + mController + + "}"; + } + + private boolean cancelTimeout() { + if (mTimeoutAction != null) { + return mTimeoutAction.cancel(); + } + return false; + } + + private void scheduleTimeout(long timeoutMillis, Runnable action) { + if (mTimeoutAction != null) { + mTimeoutAction.cancel(); + } + mTimeoutAction = new DelayedAction(mHandler, timeoutMillis, action); + } + + /** @hide */ + @VisibleForTesting + public static class DelayedAction { + private final AtomicBoolean mCompleted = new AtomicBoolean(); + private final Object mToken = new Object(); + private final Handler mHandler; + private final Runnable mAction; + + @VisibleForTesting + public DelayedAction(Handler handler, long timeoutMillis, Runnable action) { + mHandler = handler; + mAction = action; + mHandler.postDelayed(this::onTimeout, mToken, timeoutMillis); + } + + private boolean onTimeout() { + if (mCompleted.compareAndSet(false, true)) { + mAction.run(); + return true; + } + return false; + } + + /** + * Cause the timeout action to run immediately and mark as timed out. + * + * @return true if the timeout was run, false if the timeout had already been canceled + */ + @VisibleForTesting + public boolean timeoutNow() { + return onTimeout(); + } + + /** + * Attempt to cancel the timeout action (such as after a callback is made) + * + * @return true if the timeout was canceled and will not run, false if time has expired and + * the timeout action has or will run momentarily + */ + public boolean cancel() { + if (!mCompleted.compareAndSet(false, true)) { + // Whoops, too late! + return false; + } + mHandler.removeCallbacksAndMessages(mToken); + return true; + } + } +} diff --git a/core/java/android/view/ScrollCaptureSession.java b/core/java/android/view/ScrollCaptureSession.java new file mode 100644 index 000000000000..628e23fb3f5e --- /dev/null +++ b/core/java/android/view/ScrollCaptureSession.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Point; +import android.graphics.Rect; + +/** + * A session represents the scope of interaction between a {@link ScrollCaptureCallback} and the + * system during an active scroll capture operation. During the scope of a session, a callback + * will receive a series of requests for image data. Resources provided here are valid for use + * until {@link ScrollCaptureCallback#onScrollCaptureEnd(Runnable)}. + * + * @hide + */ +public class ScrollCaptureSession { + + private final Surface mSurface; + private final Rect mScrollBounds; + private final Point mPositionInWindow; + + @Nullable + private ScrollCaptureClient mClient; + + /** @hide */ + public ScrollCaptureSession(Surface surface, Rect scrollBounds, Point positionInWindow, + ScrollCaptureClient client) { + mSurface = surface; + mScrollBounds = scrollBounds; + mPositionInWindow = positionInWindow; + mClient = client; + } + + /** + * Returns a + * <a href="https://source.android.com/devices/graphics/arch-bq-gralloc">BufferQueue</a> in the + * form of a {@link Surface} for transfer of image buffers. + * + * @return the surface for transferring image buffers + * @throws IllegalStateException if the session has been closed + */ + @NonNull + public Surface getSurface() { + return mSurface; + } + + /** + * Returns the {@code scroll bounds}, as provided by + * {@link ScrollCaptureCallback#onScrollCaptureSearch}. + * + * @return the area of scrolling content within the containing view + */ + @NonNull + public Rect getScrollBounds() { + return mScrollBounds; + } + + /** + * Returns the offset of {@code scroll bounds} within the window. + * + * @return the area of scrolling content within the containing view + */ + @NonNull + public Point getPositionInWindow() { + return mPositionInWindow; + } + + /** + * Notify the system that an a buffer has been posted via the getSurface() channel. + * + * @param frameNumber the frame number of the queued buffer + * @param capturedArea the area captured, relative to scroll bounds + */ + public void notifyBufferSent(long frameNumber, @NonNull Rect capturedArea) { + if (mClient != null) { + mClient.onRequestImageCompleted(frameNumber, capturedArea); + } + } + + /** + * @hide + */ + public void disconnect() { + mClient = null; + if (mSurface.isValid()) { + mSurface.release(); + } + } +} diff --git a/core/java/android/view/ScrollCaptureTarget.java b/core/java/android/view/ScrollCaptureTarget.java new file mode 100644 index 000000000000..f3fcabb26b31 --- /dev/null +++ b/core/java/android/view/ScrollCaptureTarget.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UiThread; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.Rect; + +import com.android.internal.util.FastMath; + +/** + * A target collects the set of contextual information for a ScrollCaptureHandler discovered during + * a {@link View#dispatchScrollCaptureSearch scroll capture search}. + * + * @hide + */ +public final class ScrollCaptureTarget { + private final View mContainingView; + private final ScrollCaptureCallback mCallback; + private final Rect mLocalVisibleRect; + private final Point mPositionInWindow; + private final int mHint; + private Rect mScrollBounds; + + private final float[] mTmpFloatArr = new float[2]; + private final Matrix mMatrixViewLocalToWindow = new Matrix(); + private final Rect mTmpRect = new Rect(); + + public ScrollCaptureTarget(@NonNull View scrollTarget, @NonNull Rect localVisibleRect, + @NonNull Point positionInWindow, @NonNull ScrollCaptureCallback callback) { + mContainingView = scrollTarget; + mHint = mContainingView.getScrollCaptureHint(); + mCallback = callback; + mLocalVisibleRect = localVisibleRect; + mPositionInWindow = positionInWindow; + } + + /** @return the hint that the {@code containing view} had during the scroll capture search */ + @View.ScrollCaptureHint + public int getHint() { + return mHint; + } + + /** @return the {@link ScrollCaptureCallback} for this target */ + @NonNull + public ScrollCaptureCallback getCallback() { + return mCallback; + } + + /** @return the {@code containing view} for this {@link ScrollCaptureCallback callback} */ + @NonNull + public View getContainingView() { + return mContainingView; + } + + /** + * Returns the un-clipped, visible bounds of the containing view during the scroll capture + * search. This is used to determine on-screen area to assist in selecting the primary target. + * + * @return the visible bounds of the {@code containing view} in view-local coordinates + */ + @NonNull + public Rect getLocalVisibleRect() { + return mLocalVisibleRect; + } + + /** @return the position of the {@code containing view} within the window */ + @NonNull + public Point getPositionInWindow() { + return mPositionInWindow; + } + + /** @return the {@code scroll bounds} for this {@link ScrollCaptureCallback callback} */ + @Nullable + public Rect getScrollBounds() { + return mScrollBounds; + } + + /** + * Sets the scroll bounds rect to the intersection of provided rect and the current bounds of + * the {@code containing view}. + */ + public void setScrollBounds(@Nullable Rect scrollBounds) { + mScrollBounds = Rect.copyOrNull(scrollBounds); + if (mScrollBounds == null) { + return; + } + if (!mScrollBounds.intersect(0, 0, + mContainingView.getWidth(), mContainingView.getHeight())) { + mScrollBounds.setEmpty(); + } + } + + private static void zero(float[] pointArray) { + pointArray[0] = 0; + pointArray[1] = 0; + } + + private static void roundIntoPoint(Point pointObj, float[] pointArray) { + pointObj.x = FastMath.round(pointArray[0]); + pointObj.y = FastMath.round(pointArray[1]); + } + + /** + * Refresh the value of {@link #mLocalVisibleRect} and {@link #mPositionInWindow} based on the + * current state of the {@code containing view}. + */ + @UiThread + public void updatePositionInWindow() { + mMatrixViewLocalToWindow.reset(); + mContainingView.transformMatrixToGlobal(mMatrixViewLocalToWindow); + + zero(mTmpFloatArr); + mMatrixViewLocalToWindow.mapPoints(mTmpFloatArr); + roundIntoPoint(mPositionInWindow, mTmpFloatArr); + } + +} diff --git a/core/java/android/view/ScrollCaptureTargetResolver.java b/core/java/android/view/ScrollCaptureTargetResolver.java new file mode 100644 index 000000000000..71e82c511e2c --- /dev/null +++ b/core/java/android/view/ScrollCaptureTargetResolver.java @@ -0,0 +1,387 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.AnyThread; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UiThread; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + + +import java.util.Queue; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +/** + * Queries additional state from a list of {@link ScrollCaptureTarget targets} via asynchronous + * callbacks, then aggregates and reduces the target list to a single target, or null if no target + * is suitable. + * <p> + * The rules for selection are (in order): + * <ul> + * <li>prefer getScrollBounds(): non-empty + * <li>prefer View.getScrollCaptureHint == SCROLL_CAPTURE_HINT_INCLUDE + * <li>prefer descendants before parents + * <li>prefer larger area for getScrollBounds() (clipped to view bounds) + * </ul> + * + * <p> + * All calls to {@link ScrollCaptureCallback#onScrollCaptureSearch} are made on the main thread, + * with results are queued and consumed to the main thread as well. + * + * @see #start(Handler, long, Consumer) + * + * @hide + */ +@UiThread +public class ScrollCaptureTargetResolver { + private static final String TAG = "ScrollCaptureTargetRes"; + private static final boolean DEBUG = true; + + private final Object mLock = new Object(); + + private final Queue<ScrollCaptureTarget> mTargets; + private Handler mHandler; + private long mTimeLimitMillis; + + private Consumer<ScrollCaptureTarget> mWhenComplete; + private int mPendingBoundsRequests; + private long mDeadlineMillis; + + private ScrollCaptureTarget mResult; + private boolean mFinished; + + private boolean mStarted; + + private static int area(Rect r) { + return r.width() * r.height(); + } + + private static boolean nullOrEmpty(Rect r) { + return r == null || r.isEmpty(); + } + + /** + * Binary operator which selects the best {@link ScrollCaptureTarget}. + */ + private static ScrollCaptureTarget chooseTarget(ScrollCaptureTarget a, ScrollCaptureTarget b) { + Log.d(TAG, "chooseTarget: " + a + " or " + b); + // Nothing plus nothing is still nothing. + if (a == null && b == null) { + Log.d(TAG, "chooseTarget: (both null) return " + null); + return null; + } + // Prefer non-null. + if (a == null || b == null) { + ScrollCaptureTarget c = (a == null) ? b : a; + Log.d(TAG, "chooseTarget: (other is null) return " + c); + return c; + + } + + boolean emptyScrollBoundsA = nullOrEmpty(a.getScrollBounds()); + boolean emptyScrollBoundsB = nullOrEmpty(b.getScrollBounds()); + if (emptyScrollBoundsA || emptyScrollBoundsB) { + if (emptyScrollBoundsA && emptyScrollBoundsB) { + // Both have an empty or null scrollBounds + Log.d(TAG, "chooseTarget: (both have empty or null bounds) return " + null); + return null; + } + // Prefer the one with a non-empty scroll bounds + if (emptyScrollBoundsA) { + Log.d(TAG, "chooseTarget: (a has empty or null bounds) return " + b); + return b; + } + Log.d(TAG, "chooseTarget: (b has empty or null bounds) return " + a); + return a; + } + + final View viewA = a.getContainingView(); + final View viewB = b.getContainingView(); + + // Prefer any view with scrollCaptureHint="INCLUDE", over one without + // This is an escape hatch for the next rule (descendants first) + boolean hintIncludeA = hasIncludeHint(viewA); + boolean hintIncludeB = hasIncludeHint(viewB); + if (hintIncludeA != hintIncludeB) { + ScrollCaptureTarget c = (hintIncludeA) ? a : b; + Log.d(TAG, "chooseTarget: (has hint=INCLUDE) return " + c); + return c; + } + + // If the views are relatives, prefer the descendant. This allows implementations to + // leverage nested scrolling APIs by interacting with the innermost scrollable view (as + // would happen with touch input). + if (isDescendant(viewA, viewB)) { + Log.d(TAG, "chooseTarget: (b is descendant of a) return " + b); + return b; + } + if (isDescendant(viewB, viewA)) { + Log.d(TAG, "chooseTarget: (a is descendant of b) return " + a); + return a; + } + + // finally, prefer one with larger scroll bounds + int scrollAreaA = area(a.getScrollBounds()); + int scrollAreaB = area(b.getScrollBounds()); + ScrollCaptureTarget c = (scrollAreaA >= scrollAreaB) ? a : b; + Log.d(TAG, "chooseTarget: return " + c); + return c; + } + + /** + * Creates an instance to query and filter {@code target}. + * + * @param targets a list of {@link ScrollCaptureTarget} as collected by {@link + * View#dispatchScrollCaptureSearch}. + * @param uiHandler the UI thread handler for the view tree + * @see #start(long, Consumer) + */ + public ScrollCaptureTargetResolver(Queue<ScrollCaptureTarget> targets) { + mTargets = targets; + } + + void checkThread() { + if (mHandler.getLooper() != Looper.myLooper()) { + throw new IllegalStateException("Called from wrong thread! (" + + Thread.currentThread().getName() + ")"); + } + } + + /** + * Blocks until a result is returned (after completion or timeout). + * <p> + * For testing only. Normal usage should receive a callback after calling {@link #start}. + */ + @VisibleForTesting + public ScrollCaptureTarget waitForResult() throws InterruptedException { + synchronized (mLock) { + while (!mFinished) { + mLock.wait(); + } + } + return mResult; + } + + + private void supplyResult(ScrollCaptureTarget target) { + checkThread(); + if (mFinished) { + return; + } + mResult = chooseTarget(mResult, target); + boolean finish = mPendingBoundsRequests == 0 + || SystemClock.elapsedRealtime() >= mDeadlineMillis; + if (finish) { + System.err.println("We think we're done, or timed out"); + mPendingBoundsRequests = 0; + mWhenComplete.accept(mResult); + synchronized (mLock) { + mFinished = true; + mLock.notify(); + } + mWhenComplete = null; + } + } + + /** + * Asks all targets for {@link ScrollCaptureCallback#onScrollCaptureSearch(Consumer) + * scrollBounds}, and selects the primary target according to the {@link + * #chooseTarget} function. + * + * @param timeLimitMillis the amount of time to wait for all responses before delivering the top + * result + * @param resultConsumer the consumer to receive the primary target + */ + @AnyThread + public void start(Handler uiHandler, long timeLimitMillis, + Consumer<ScrollCaptureTarget> resultConsumer) { + synchronized (mLock) { + if (mStarted) { + throw new IllegalStateException("already started!"); + } + if (timeLimitMillis < 0) { + throw new IllegalArgumentException("Time limit must be positive"); + } + mHandler = uiHandler; + mTimeLimitMillis = timeLimitMillis; + mWhenComplete = resultConsumer; + if (mTargets.isEmpty()) { + mHandler.post(() -> supplyResult(null)); + return; + } + mStarted = true; + uiHandler.post(() -> run(timeLimitMillis, resultConsumer)); + } + } + + + private void run(long timeLimitMillis, Consumer<ScrollCaptureTarget> resultConsumer) { + checkThread(); + + mPendingBoundsRequests = mTargets.size(); + for (ScrollCaptureTarget target : mTargets) { + queryTarget(target); + } + mDeadlineMillis = SystemClock.elapsedRealtime() + mTimeLimitMillis; + mHandler.postAtTime(mTimeoutRunnable, mDeadlineMillis); + } + + private final Runnable mTimeoutRunnable = new Runnable() { + @Override + public void run() { + checkThread(); + supplyResult(null); + } + }; + + + /** + * Adds a target to the list and requests {@link ScrollCaptureCallback#onScrollCaptureSearch} + * scrollBounds} from it. Results are returned by a call to {@link #onScrollBoundsProvided}. + * + * @param target the target to add + */ + @UiThread + private void queryTarget(@NonNull ScrollCaptureTarget target) { + checkThread(); + final ScrollCaptureCallback callback = target.getCallback(); + // from the UI thread, request scroll bounds + callback.onScrollCaptureSearch( + // allow only one callback to onReady.accept(): + new SingletonConsumer<Rect>( + // Queue and consume on the UI thread + ((scrollBounds) -> mHandler.post( + () -> onScrollBoundsProvided(target, scrollBounds))))); + + } + + @UiThread + private void onScrollBoundsProvided(ScrollCaptureTarget target, @Nullable Rect scrollBounds) { + checkThread(); + if (mFinished) { + return; + } + + // Record progress. + mPendingBoundsRequests--; + + // Remove the timeout. + mHandler.removeCallbacks(mTimeoutRunnable); + + boolean doneOrTimedOut = mPendingBoundsRequests == 0 + || SystemClock.elapsedRealtime() >= mDeadlineMillis; + + final View containingView = target.getContainingView(); + if (!nullOrEmpty(scrollBounds) && containingView.isAggregatedVisible()) { + target.updatePositionInWindow(); + target.setScrollBounds(scrollBounds); + supplyResult(target); + } + + System.err.println("mPendingBoundsRequests: " + mPendingBoundsRequests); + System.err.println("mDeadlineMillis: " + mDeadlineMillis); + System.err.println("SystemClock.elapsedRealtime(): " + SystemClock.elapsedRealtime()); + + if (!mFinished) { + // Reschedule the timeout. + System.err.println( + "We think we're NOT done yet and will check back at " + mDeadlineMillis); + mHandler.postAtTime(mTimeoutRunnable, mDeadlineMillis); + } + } + + private static boolean hasIncludeHint(View view) { + return (view.getScrollCaptureHint() & View.SCROLL_CAPTURE_HINT_INCLUDE) != 0; + } + + /** + * Determines if {@code otherView} is a descendant of {@code view}. + * + * @param view a view + * @param otherView another view + * @return true if {@code view} is an ancestor of {@code otherView} + */ + private static boolean isDescendant(@NonNull View view, @NonNull View otherView) { + if (view == otherView) { + return false; + } + ViewParent otherParent = otherView.getParent(); + while (otherParent != view && otherParent != null) { + otherParent = otherParent.getParent(); + } + return otherParent == view; + } + + private static int findRelation(@NonNull View a, @NonNull View b) { + if (a == b) { + return 0; + } + + ViewParent parentA = a.getParent(); + ViewParent parentB = b.getParent(); + + while (parentA != null || parentB != null) { + if (parentA == parentB) { + return 0; + } + if (parentA == b) { + return 1; // A is descendant of B + } + if (parentB == a) { + return -1; // B is descendant of A + } + if (parentA != null) { + parentA = parentA.getParent(); + } + if (parentB != null) { + parentB = parentB.getParent(); + } + } + return 0; + } + + /** + * A safe wrapper for a consumer callbacks intended to accept a single value. It ensures + * that the receiver of the consumer does not retain a reference to {@code target} after use nor + * cause race conditions by invoking {@link Consumer#accept accept} more than once. + * + * @param target the target consumer + */ + static class SingletonConsumer<T> implements Consumer<T> { + final AtomicReference<Consumer<T>> mAtomicRef; + + SingletonConsumer(Consumer<T> target) { + mAtomicRef = new AtomicReference<>(target); + } + + @Override + public void accept(T t) { + final Consumer<T> consumer = mAtomicRef.getAndSet(null); + if (consumer != null) { + consumer.accept(t); + } + } + } +} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 8abe72fc91e8..f98c1f660cfa 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -144,6 +144,7 @@ import android.widget.ScrollBarDrawable; import com.android.internal.R; import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.view.ScrollCaptureInternal; import com.android.internal.view.TooltipPopup; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.widget.ScrollBarUtils; @@ -167,6 +168,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Queue; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; @@ -1311,7 +1313,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public static final int AUTOFILL_TYPE_LIST = 3; - /** * Autofill type for a field that contains a date, which is represented by a long representing * the number of milliseconds since the standard base time known as "the epoch", namely @@ -1441,6 +1442,58 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public static final int IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS = 0x8; + /** {@hide} */ + @IntDef(flag = true, prefix = {"SCROLL_CAPTURE_HINT_"}, + value = { + SCROLL_CAPTURE_HINT_AUTO, + SCROLL_CAPTURE_HINT_EXCLUDE, + SCROLL_CAPTURE_HINT_INCLUDE, + SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScrollCaptureHint {} + + /** + * The content of this view will be considered for scroll capture if scrolling is possible. + * + * @see #getScrollCaptureHint() + * @see #setScrollCaptureHint(int) + * @hide + */ + public static final int SCROLL_CAPTURE_HINT_AUTO = 0; + + /** + * Explicitly exclcude this view as a potential scroll capture target. The system will not + * consider it. Mutually exclusive with {@link #SCROLL_CAPTURE_HINT_INCLUDE}, which this flag + * takes precedence over. + * + * @see #getScrollCaptureHint() + * @see #setScrollCaptureHint(int) + * @hide + */ + public static final int SCROLL_CAPTURE_HINT_EXCLUDE = 0x1; + + /** + * Explicitly include this view as a potential scroll capture target. When locating a scroll + * capture target, this view will be prioritized before others without this flag. Mutually + * exclusive with {@link #SCROLL_CAPTURE_HINT_EXCLUDE}, which takes precedence. + * + * @see #getScrollCaptureHint() + * @see #setScrollCaptureHint(int) + * @hide + */ + public static final int SCROLL_CAPTURE_HINT_INCLUDE = 0x2; + + /** + * Explicitly exclude all children of this view as potential scroll capture targets. This view + * is unaffected. Note: Excluded children are not considered, regardless of {@link + * #SCROLL_CAPTURE_HINT_INCLUDE}. + * + * @see #getScrollCaptureHint() + * @see #setScrollCaptureHint(int) + * @hide + */ + public static final int SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS = 0x4; /** * This view is enabled. Interpretation varies by subclass. @@ -3430,6 +3483,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * 11 PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK * 1 PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS * 1 PFLAG4_AUTOFILL_HIDE_HIGHLIGHT + * 11 PFLAG4_SCROLL_CAPTURE_HINT_MASK * |-------|-------|-------|-------| */ @@ -3477,6 +3531,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static final int PFLAG4_AUTOFILL_HIDE_HIGHLIGHT = 0x200; + /** + * Shift for the bits in {@link #mPrivateFlags4} related to scroll capture. + */ + static final int PFLAG4_SCROLL_CAPTURE_HINT_SHIFT = 10; + + static final int PFLAG4_SCROLL_CAPTURE_HINT_MASK = (SCROLL_CAPTURE_HINT_INCLUDE + | SCROLL_CAPTURE_HINT_EXCLUDE | SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS) + << PFLAG4_SCROLL_CAPTURE_HINT_SHIFT; + /* End of masks for mPrivateFlags4 */ /** @hide */ @@ -4690,6 +4753,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Used to track {@link #mSystemGestureExclusionRects} */ public RenderNode.PositionUpdateListener mPositionUpdateListener; + + /** + * Allows the application to implement custom scroll capture support. + */ + ScrollCaptureCallback mScrollCaptureCallback; } @UnsupportedAppUsage @@ -5941,6 +6009,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, case R.styleable.View_forceDarkAllowed: mRenderNode.setForceDarkAllowed(a.getBoolean(attr, true)); break; + case R.styleable.View_scrollCaptureHint: + setScrollCaptureHint((a.getInt(attr, SCROLL_CAPTURE_HINT_AUTO))); + break; } } @@ -29091,6 +29162,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int mLeashedParentAccessibilityViewId; /** + * + */ + ScrollCaptureInternal mScrollCaptureInternal; + + /** * Creates a new set of attachment information with the specified * events handler and thread. * @@ -29150,6 +29226,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return events; } + + @Nullable + ScrollCaptureInternal getScrollCaptureInternal() { + if (mScrollCaptureInternal != null) { + mScrollCaptureInternal = new ScrollCaptureInternal(); + } + return mScrollCaptureInternal; + } } /** @@ -29683,6 +29767,104 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } + + /** + * Returns the current scroll capture hint for this view. + * + * @return the current scroll capture hint + * + * @hide + */ + @ScrollCaptureHint + public int getScrollCaptureHint() { + return (mPrivateFlags4 & PFLAG4_SCROLL_CAPTURE_HINT_MASK) + >> PFLAG4_SCROLL_CAPTURE_HINT_SHIFT; + } + + /** + * Sets the scroll capture hint for this View. These flags affect the search for a potential + * scroll capture targets. + * + * @param hint the scrollCaptureHint flags value to set + * + * @hide + */ + public void setScrollCaptureHint(@ScrollCaptureHint int hint) { + mPrivateFlags4 &= ~PFLAG4_SCROLL_CAPTURE_HINT_MASK; + mPrivateFlags4 |= ((hint << PFLAG4_SCROLL_CAPTURE_HINT_SHIFT) + & PFLAG4_SCROLL_CAPTURE_HINT_MASK); + } + + /** + * Sets the callback to receive scroll capture requests. This component is the adapter between + * the scroll capture API and application UI code. If no callback is set, the system may provide + * an implementation. Any value provided here will take precedence over a system version. + * <p> + * This view will be ignored when {@link #SCROLL_CAPTURE_HINT_EXCLUDE} is set in its {@link + * #setScrollCaptureHint(int) scrollCaptureHint}, regardless whether a callback has been set. + * <p> + * It is recommended to set the scroll capture hint {@link #SCROLL_CAPTURE_HINT_INCLUDE} when + * setting a custom callback to help ensure it is selected as the target. + * + * @param callback the new callback to assign + * + * @hide + */ + public void setScrollCaptureCallback(@Nullable ScrollCaptureCallback callback) { + getListenerInfo().mScrollCaptureCallback = callback; + } + + /** {@hide} */ + @Nullable + public ScrollCaptureCallback createScrollCaptureCallbackInternal(@NonNull Rect localVisibleRect, + @NonNull Point windowOffset) { + if (mAttachInfo == null) { + return null; + } + if (mAttachInfo.mScrollCaptureInternal == null) { + mAttachInfo.mScrollCaptureInternal = new ScrollCaptureInternal(); + } + return mAttachInfo.mScrollCaptureInternal.requestCallback(this, localVisibleRect, + windowOffset); + } + + /** + * Called when scroll capture is requested, to search for appropriate content to scroll. If + * applicable, this view adds itself to the provided list for consideration, subject to the + * flags set by {@link #setScrollCaptureHint}. + * + * @param localVisibleRect the local visible rect of this view + * @param windowOffset the offset of localVisibleRect within the window + * @param targets a queue which collects potential targets + * + * @throws IllegalStateException if this view is not attached to a window + * @hide + */ + public void dispatchScrollCaptureSearch(@NonNull Rect localVisibleRect, + @NonNull Point windowOffset, @NonNull Queue<ScrollCaptureTarget> targets) { + int hint = getScrollCaptureHint(); + if ((hint & SCROLL_CAPTURE_HINT_EXCLUDE) != 0) { + return; + } + + // Get a callback provided by the framework, library or application. + ScrollCaptureCallback callback = + (mListenerInfo == null) ? null : mListenerInfo.mScrollCaptureCallback; + + // Try internal support for standard scrolling containers. + if (callback == null) { + callback = createScrollCaptureCallbackInternal(localVisibleRect, windowOffset); + } + + // If found, then add it to the list. + if (callback != null) { + // Add to the list for consideration + Point offset = new Point(windowOffset.x, windowOffset.y); + Rect rect = new Rect(localVisibleRect); + targets.add(new ScrollCaptureTarget(this, rect, offset, callback)); + } + } + /** * Dump all private flags in readable format, useful for documentation and * sanity checking. diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index e34e84c977ea..7935eb1ffc39 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -40,6 +40,7 @@ import android.graphics.Color; import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; @@ -75,6 +76,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.function.Predicate; /** @@ -188,7 +190,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private PointF mLocalPoint; // Lazily-created holder for point computations. - private float[] mTempPoint; + private float[] mTempPosition; + + // Lazily-created holder for point computations. + private Point mTempPoint; + + // Lazily created Rect for dispatch to children + private Rect mTempRect; + + // Lazily created int[2] for dispatch to children + private int[] mTempLocation; // Layout animation private LayoutAnimationController mLayoutAnimationController; @@ -1860,7 +1871,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final float tx = mCurrentDragStartEvent.mX; final float ty = mCurrentDragStartEvent.mY; - final float[] point = getTempPoint(); + final float[] point = getTempLocationF(); point[0] = tx; point[1] = ty; transformPointToViewLocal(point, child); @@ -2932,9 +2943,23 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } - private float[] getTempPoint() { + private Rect getTempRect() { + if (mTempRect == null) { + mTempRect = new Rect(); + } + return mTempRect; + } + + private float[] getTempLocationF() { + if (mTempPosition == null) { + mTempPosition = new float[2]; + } + return mTempPosition; + } + + private Point getTempPoint() { if (mTempPoint == null) { - mTempPoint = new float[2]; + mTempPoint = new Point(); } return mTempPoint; } @@ -2948,7 +2973,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @UnsupportedAppUsage protected boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) { - final float[] point = getTempPoint(); + final float[] point = getTempLocationF(); point[0] = x; point[1] = y; transformPointToViewLocal(point, child); @@ -4568,7 +4593,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final boolean nonActionable = !child.isClickable() && !child.isLongClickable(); final boolean duplicatesState = (child.mViewFlags & DUPLICATE_PARENT_STATE) != 0; if (nonActionable || duplicatesState) { - final float[] point = getTempPoint(); + final float[] point = getTempLocationF(); point[0] = x; point[1] = y; transformPointToViewLocal(point, child); @@ -7354,6 +7379,97 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Offsets the given rectangle in parent's local coordinates into child's coordinate space + * and clips the result to the child View's bounds, padding and clipRect if appropriate. If the + * resulting rectangle is not empty, the request is forwarded to the child. + * <p> + * Note: This method does not account for any static View transformations which may be + * applied to the child view. + * + * @param child the child to dispatch to + * @param localVisibleRect the visible (clipped) area of this ViewGroup, in local coordinates + * @param windowOffset the offset of localVisibleRect within the window + * @param targets a queue to collect located targets + */ + private void dispatchTransformedScrollCaptureSearch(View child, Rect localVisibleRect, + Point windowOffset, Queue<ScrollCaptureTarget> targets) { + + // copy local visible rect for modification and dispatch + final Rect childVisibleRect = getTempRect(); + childVisibleRect.set(localVisibleRect); + + // transform to child coords + final Point childWindowOffset = getTempPoint(); + childWindowOffset.set(windowOffset.x, windowOffset.y); + + final int dx = child.mLeft - mScrollX; + final int dy = child.mTop - mScrollY; + + childVisibleRect.offset(-dx, -dy); + childWindowOffset.offset(dx, dy); + + boolean rectIsVisible = true; + final int width = mRight - mLeft; + final int height = mBottom - mTop; + + // Clip to child bounds + if (getClipChildren()) { + rectIsVisible = childVisibleRect.intersect(0, 0, child.getWidth(), child.getHeight()); + } + + // Clip to child padding. + if (rectIsVisible && (child instanceof ViewGroup) + && ((ViewGroup) child).getClipToPadding()) { + rectIsVisible = childVisibleRect.intersect( + child.mPaddingLeft, child.mPaddingTop, + child.getWidth() - child.mPaddingRight, + child.getHeight() - child.mPaddingBottom); + } + // Clip to child clipBounds. + if (rectIsVisible && child.mClipBounds != null) { + rectIsVisible = childVisibleRect.intersect(child.mClipBounds); + } + if (rectIsVisible) { + child.dispatchScrollCaptureSearch(childVisibleRect, childWindowOffset, targets); + } + } + + /** + * Handle the scroll capture search request by checking this view if applicable, then to each + * child view. + * + * @param localVisibleRect the visible area of this ViewGroup in local coordinates, according to + * the parent + * @param windowOffset the offset of this view within the window + * @param targets the collected list of scroll capture targets + * + * @hide + */ + @Override + public void dispatchScrollCaptureSearch( + @NonNull Rect localVisibleRect, @NonNull Point windowOffset, + @NonNull Queue<ScrollCaptureTarget> targets) { + + // Dispatch to self first. + super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets); + + // Then dispatch to children, if not excluding descendants. + if ((getScrollCaptureHint() & SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS) == 0) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + // Only visible views can be captured. + if (child.getVisibility() != View.VISIBLE) { + continue; + } + // Transform to child coords and dispatch + dispatchTransformedScrollCaptureSearch(child, localVisibleRect, windowOffset, + targets); + } + } + } + + /** * Returns the animation listener to which layout animation events are * sent. * diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index ed1edc3bd526..9d275cdcb00f 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -206,6 +206,7 @@ public final class ViewRootImpl implements ViewParent, private static final boolean DEBUG_INPUT_STAGES = false || LOCAL_LOGV; private static final boolean DEBUG_KEEP_SCREEN_ON = false || LOCAL_LOGV; private static final boolean DEBUG_CONTENT_CAPTURE = false || LOCAL_LOGV; + private static final boolean DEBUG_SCROLL_CAPTURE = false || LOCAL_LOGV; /** * Set to false if we do not want to use the multi threaded renderer even though @@ -653,6 +654,8 @@ public final class ViewRootImpl implements ViewParent, private final InsetsController mInsetsController; private final ImeFocusController mImeFocusController; + private ScrollCaptureClient mScrollCaptureClient; + /** * @return {@link ImeFocusController} for this instance. */ @@ -661,6 +664,11 @@ public final class ViewRootImpl implements ViewParent, return mImeFocusController; } + /** @return The current {@link ScrollCaptureClient} for this instance, if any is active. */ + @Nullable + public ScrollCaptureClient getScrollCaptureClient() { + return mScrollCaptureClient; + } private final GestureExclusionTracker mGestureExclusionTracker = new GestureExclusionTracker(); @@ -694,6 +702,8 @@ public final class ViewRootImpl implements ViewParent, // draw returns. private SurfaceControl.Transaction mRtBLASTSyncTransaction = new SurfaceControl.Transaction(); + private HashSet<ScrollCaptureCallback> mRootScrollCaptureCallbacks; + private String mTag = TAG; public ViewRootImpl(Context context, Display display) { @@ -3769,7 +3779,9 @@ public final class ViewRootImpl implements ViewParent, mNextReportConsumeBLAST = true; mNextDrawUseBLASTSyncTransaction = false; - mBlastBufferQueue.setNextTransaction(mRtBLASTSyncTransaction); + if (mBlastBufferQueue != null) { + mBlastBufferQueue.setNextTransaction(mRtBLASTSyncTransaction); + } } boolean canUseAsync = draw(fullRedrawNeeded); if (usingAsyncReport && !canUseAsync) { @@ -4778,6 +4790,7 @@ public final class ViewRootImpl implements ViewParent, private static final int MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED = 33; private static final int MSG_SHOW_INSETS = 34; private static final int MSG_HIDE_INSETS = 35; + private static final int MSG_REQUEST_SCROLL_CAPTURE = 36; final class ViewRootHandler extends Handler { @@ -5080,6 +5093,9 @@ public final class ViewRootImpl implements ViewParent, case MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED: { updateLocationInParentDisplay(msg.arg1, msg.arg2); } break; + case MSG_REQUEST_SCROLL_CAPTURE: + handleScrollCaptureRequest((IScrollCaptureController) msg.obj); + break; } } } @@ -8789,6 +8805,131 @@ public final class ViewRootImpl implements ViewParent, return false; } + /** + * Adds a scroll capture callback to this window. + * + * @param callback the callback to add + */ + public void addScrollCaptureCallback(ScrollCaptureCallback callback) { + if (mRootScrollCaptureCallbacks == null) { + mRootScrollCaptureCallbacks = new HashSet<>(); + } + mRootScrollCaptureCallbacks.add(callback); + } + + /** + * Removes a scroll capture callback from this window. + * + * @param callback the callback to remove + */ + public void removeScrollCaptureCallback(ScrollCaptureCallback callback) { + if (mRootScrollCaptureCallbacks != null) { + mRootScrollCaptureCallbacks.remove(callback); + if (mRootScrollCaptureCallbacks.isEmpty()) { + mRootScrollCaptureCallbacks = null; + } + } + } + + /** + * Dispatches a scroll capture request to the view hierarchy on the ui thread. + * + * @param controller the controller to receive replies + */ + public void dispatchScrollCaptureRequest(@NonNull IScrollCaptureController controller) { + mHandler.obtainMessage(MSG_REQUEST_SCROLL_CAPTURE, controller).sendToTarget(); + } + + /** + * Collect and include any ScrollCaptureCallback instances registered with the window. + * + * @see #addScrollCaptureCallback(ScrollCaptureCallback) + * @param targets the search queue for targets + */ + private void collectRootScrollCaptureTargets(Queue<ScrollCaptureTarget> targets) { + for (ScrollCaptureCallback cb : mRootScrollCaptureCallbacks) { + // Add to the list for consideration + Point offset = new Point(mView.getLeft(), mView.getTop()); + Rect rect = new Rect(0, 0, mView.getWidth(), mView.getHeight()); + targets.add(new ScrollCaptureTarget(mView, rect, offset, cb)); + } + } + + /** + * Handles an inbound request for scroll capture from the system. If a client is not already + * active, a search will be dispatched through the view tree to locate scrolling content. + * <p> + * Either {@link IScrollCaptureController#onClientConnected(IScrollCaptureClient, Rect, + * Point)} or {@link IScrollCaptureController#onClientUnavailable()} will be returned + * depending on the results of the search. + * + * @param controller the interface to the system controller + * @see ScrollCaptureTargetResolver + */ + private void handleScrollCaptureRequest(@NonNull IScrollCaptureController controller) { + LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + + // Window (root) level callbacks + collectRootScrollCaptureTargets(targetList); + + // Search through View-tree + View rootView = getView(); + Point point = new Point(); + Rect rect = new Rect(0, 0, rootView.getWidth(), rootView.getHeight()); + getChildVisibleRect(rootView, rect, point); + rootView.dispatchScrollCaptureSearch(rect, point, targetList); + + // No-op path. Scroll capture not offered for this window. + if (targetList.isEmpty()) { + dispatchScrollCaptureSearchResult(controller, null); + return; + } + + // Request scrollBounds from each of the targets. + // Continues with the consumer once all responses are consumed, or the timeout expires. + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetList); + resolver.start(mHandler, 1000, + (selected) -> dispatchScrollCaptureSearchResult(controller, selected)); + } + + /** Called by {@link #handleScrollCaptureRequest} when a result is returned */ + private void dispatchScrollCaptureSearchResult( + @NonNull IScrollCaptureController controller, + @Nullable ScrollCaptureTarget selectedTarget) { + + // If timeout or no eligible targets found. + if (selectedTarget == null) { + try { + if (DEBUG_SCROLL_CAPTURE) { + Log.d(TAG, "scrollCaptureSearch returned no targets available."); + } + controller.onClientUnavailable(); + } catch (RemoteException e) { + if (DEBUG_SCROLL_CAPTURE) { + Log.w(TAG, "Failed to notify controller of scroll capture search result.", e); + } + } + return; + } + + // Create a client instance and return it to the caller + mScrollCaptureClient = new ScrollCaptureClient(selectedTarget, controller); + try { + if (DEBUG_SCROLL_CAPTURE) { + Log.d(TAG, "scrollCaptureSearch returning client: " + getScrollCaptureClient()); + } + controller.onClientConnected( + mScrollCaptureClient, + selectedTarget.getScrollBounds(), + selectedTarget.getPositionInWindow()); + } catch (RemoteException e) { + if (DEBUG_SCROLL_CAPTURE) { + Log.w(TAG, "Failed to notify controller of scroll capture search result.", e); + } + mScrollCaptureClient.disconnect(); + mScrollCaptureClient = null; + } + } private void reportNextDraw() { if (mReportNextDraw == false) { @@ -9091,6 +9232,13 @@ public final class ViewRootImpl implements ViewParent, } } + @Override + public void requestScrollCapture(IScrollCaptureController controller) { + final ViewRootImpl viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchScrollCaptureRequest(controller); + } + } } public static final class CalledFromWrongThreadException extends AndroidRuntimeException { diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index ae9afabad533..b1536484b515 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -2535,6 +2535,33 @@ public abstract class Window { return Collections.emptyList(); } + /** + * System request to begin scroll capture. + * + * @param controller the controller to receive responses + * @hide + */ + public void requestScrollCapture(IScrollCaptureController controller) { + } + + /** + * Registers a {@link ScrollCaptureCallback} with the root of this window. + * + * @param callback the callback to add + * @hide + */ + public void addScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) { + } + + /** + * Unregisters a {@link ScrollCaptureCallback} previously registered with this window. + * + * @param callback the callback to remove + * @hide + */ + public void removeScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) { + } + /** @hide */ public void setTheme(int resId) { } diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index ec5130143086..397bce44b25e 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -24,7 +24,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.util.MergedConfiguration; -import android.view.IWindowSession; import java.util.HashMap; diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index d851a099d0e1..970bab9bc53b 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -2611,11 +2611,12 @@ public class ChooserActivity extends ResolverActivity implements * does not match either the personal or work user handle. **/ private int getProfileForUser(UserHandle currentUserHandle) { - if (currentUserHandle == getPersonalProfileUserHandle()) { + if (currentUserHandle.equals(getPersonalProfileUserHandle())) { return PROFILE_PERSONAL; - } else if (currentUserHandle == getWorkProfileUserHandle()) { + } else if (currentUserHandle.equals(getWorkProfileUserHandle())) { return PROFILE_WORK; } + Log.e(TAG, "User " + currentUserHandle + " does not belong to a personal or work profile."); return -1; } diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java index 36eecfb685e8..3eb0923363f2 100644 --- a/core/java/com/android/internal/app/IntentForwarderActivity.java +++ b/core/java/com/android/internal/app/IntentForwarderActivity.java @@ -163,7 +163,7 @@ public class IntentForwarderActivity extends Activity { return; } sanitizeIntent(innerIntent); - startActivity(intentReceived); + startActivityAsCaller(intentReceived, null, null, false, getUserId()); finish(); } @@ -234,23 +234,7 @@ public class IntentForwarderActivity extends Activity { Intent intentToCheck = forwardIntent; if (Intent.ACTION_CHOOSER.equals(forwardIntent.getAction())) { - // The EXTRA_INITIAL_INTENTS may not be allowed to be forwarded. - if (forwardIntent.hasExtra(Intent.EXTRA_INITIAL_INTENTS)) { - Slog.wtf(TAG, "An chooser intent with extra initial intents cannot be forwarded to" - + " a different user"); - return null; - } - if (forwardIntent.hasExtra(Intent.EXTRA_REPLACEMENT_EXTRAS)) { - Slog.wtf(TAG, "A chooser intent with replacement extras cannot be forwarded to a" - + " different user"); - return null; - } - intentToCheck = forwardIntent.getParcelableExtra(Intent.EXTRA_INTENT); - if (intentToCheck == null) { - Slog.wtf(TAG, "Cannot forward a chooser intent with no extra " - + Intent.EXTRA_INTENT); - return null; - } + return null; } if (forwardIntent.getSelector() != null) { intentToCheck = forwardIntent.getSelector(); diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java index 25c114f4b7c1..23ba6530b072 100644 --- a/core/java/com/android/internal/policy/PhoneWindow.java +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -79,6 +79,7 @@ import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.IRotationWatcher.Stub; +import android.view.IScrollCaptureController; import android.view.IWindowManager; import android.view.InputDevice; import android.view.InputEvent; @@ -89,6 +90,7 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.ScrollCaptureCallback; import android.view.SearchEvent; import android.view.SurfaceHolder.Callback2; import android.view.View; @@ -3916,4 +3918,35 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { : null); } } + + /** + * System request to begin scroll capture. + * + * @param controller the controller to receive responses + * @hide + */ + @Override + public void requestScrollCapture(IScrollCaptureController controller) { + getViewRootImpl().dispatchScrollCaptureRequest(controller); + } + + /** + * Registers a handler providing scrolling capture support for window content. + * + * @param callback the callback to add + */ + @Override + public void addScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) { + getViewRootImpl().addScrollCaptureCallback(callback); + } + + /** + * Unregisters the given {@link ScrollCaptureCallback}. + * + * @param callback the callback to remove + */ + @Override + public void removeScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) { + getViewRootImpl().removeScrollCaptureCallback(callback); + } } diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java index 47f094f292f2..7f3eb4515654 100644 --- a/core/java/com/android/internal/view/BaseIWindow.java +++ b/core/java/com/android/internal/view/BaseIWindow.java @@ -26,6 +26,7 @@ import android.os.RemoteException; import android.util.MergedConfiguration; import android.view.DisplayCutout; import android.view.DragEvent; +import android.view.IScrollCaptureController; import android.view.IWindow; import android.view.IWindowSession; import android.view.InsetsSourceControl; @@ -169,4 +170,13 @@ public class BaseIWindow extends IWindow.Stub { @Override public void dispatchPointerCaptureChanged(boolean hasCapture) { } + + @Override + public void requestScrollCapture(IScrollCaptureController controller) { + try { + controller.onClientUnavailable(); + } catch (RemoteException ex) { + // ignore + } + } } diff --git a/core/java/com/android/internal/view/ScrollCaptureInternal.java b/core/java/com/android/internal/view/ScrollCaptureInternal.java new file mode 100644 index 000000000000..c589afdeaa1a --- /dev/null +++ b/core/java/com/android/internal/view/ScrollCaptureInternal.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2020 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.internal.view; + +import android.annotation.Nullable; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.ScrollCaptureCallback; +import android.view.View; +import android.view.ViewGroup; + +/** + * Provides built-in framework level Scroll Capture support for standard scrolling Views. + */ +public class ScrollCaptureInternal { + private static final String TAG = "ScrollCaptureInternal"; + + private static final int UP = -1; + private static final int DOWN = 1; + + /** + * Not a ViewGroup, or cannot scroll according to View APIs. + */ + public static final int TYPE_FIXED = 0; + + /** + * Slides a single child view using mScrollX/mScrollY. + */ + public static final int TYPE_SCROLLING = 1; + + /** + * Slides child views through the viewport by translating their layout positions with {@link + * View#offsetTopAndBottom(int)}. Manages Child view lifecycle, creating as needed and + * binding views to data from an adapter. Views are reused whenever possible. + */ + public static final int TYPE_RECYCLING = 2; + + /** + * Performs tests on the given View and determines: + * 1. If scrolling is possible + * 2. What mechanisms are used for scrolling. + * <p> + * This needs to be fast and not alloc memory. It's called on everything in the tree not marked + * as excluded during scroll capture search. + */ + public static int detectScrollingType(View view) { + // Must be a ViewGroup + if (!(view instanceof ViewGroup)) { + return TYPE_FIXED; + } + // Confirm that it can scroll. + if (!(view.canScrollVertically(DOWN) || view.canScrollVertically(UP))) { + // Nothing to scroll here, move along. + return TYPE_FIXED; + } + // ScrollViews accept only a single child. + if (((ViewGroup) view).getChildCount() > 1) { + return TYPE_RECYCLING; + } + //Because recycling containers don't use scrollY, a non-zero value means Scroll view. + if (view.getScrollY() != 0) { + return TYPE_SCROLLING; + } + // Since scrollY cannot be negative, this means a Recycling view. + if (view.canScrollVertically(UP)) { + return TYPE_RECYCLING; + } + // canScrollVertically(UP) == false, getScrollY() == 0, getChildCount() == 1. + + // For Recycling containers, this should be a no-op (RecyclerView logs a warning) + view.scrollTo(view.getScrollX(), 1); + + // A scrolling container would have moved by 1px. + if (view.getScrollY() == 1) { + view.scrollTo(view.getScrollX(), 0); + return TYPE_SCROLLING; + } + return TYPE_RECYCLING; + } + + /** + * Creates a scroll capture callback for the given view if possible. + * + * @param view the view to capture + * @param localVisibleRect the visible area of the given view in local coordinates, as supplied + * by the view parent + * @param positionInWindow the offset of localVisibleRect within the window + * + * @return a new callback or null if the View isn't supported + */ + @Nullable + public ScrollCaptureCallback requestCallback(View view, Rect localVisibleRect, + Point positionInWindow) { + // Nothing to see here yet. + int i = detectScrollingType(view); + switch (i) { + case TYPE_SCROLLING: + return new ScrollCaptureViewSupport<>((ViewGroup) view, + new ScrollViewCaptureHelper()); + } + return null; + } +} diff --git a/core/java/com/android/internal/view/ScrollCaptureViewHelper.java b/core/java/com/android/internal/view/ScrollCaptureViewHelper.java new file mode 100644 index 000000000000..9f100bd6440f --- /dev/null +++ b/core/java/com/android/internal/view/ScrollCaptureViewHelper.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 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.internal.view; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Rect; +import android.view.View; + +interface ScrollCaptureViewHelper<V extends View> { + int UP = -1; + int DOWN = 1; + + /** + * Verifies that the view is still visible and scrollable. If true is returned here, expect a + * call to {@link #onComputeScrollBounds(View)} to follow. + * + * @param view the view being captured + * @return true if the callback should respond to a request with scroll bounds + */ + default boolean onAcceptSession(@Nullable V view) { + return view != null && view.isVisibleToUser() + && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN)); + } + + /** + * Given a scroll capture request for a view, adjust the provided rect to cover the scrollable + * content area. The default implementation returns the padded content area of {@code view}. + * + * @param view the view being captured + */ + default Rect onComputeScrollBounds(@Nullable V view) { + return new Rect(view.getPaddingLeft(), view.getPaddingTop(), + view.getWidth() - view.getPaddingRight(), + view.getHeight() - view.getPaddingBottom()); + } + /** + * Adjust the target for capture. + * <p> + * Do not touch anything that may change layout positions or sizes on screen. Anything else may + * be adjusted as long as it can be reversed in {@link #onPrepareForEnd(View)}. + * + * @param view the view being captured + * @param scrollBounds the bounds within {@code view} where content scrolls + */ + void onPrepareForStart(@NonNull V view, Rect scrollBounds); + + /** + * Map the request onto the screen. + * <p> + * Given a rect describing the area to capture, relative to scrollBounds, take actions + * necessary to bring the content within the rectangle into the visible area of the view if + * needed and return the resulting rectangle describing the position and bounds of the area + * which is visible. + * + * @param scrollBounds the area in which scrolling content moves, local to the {@code containing + * view} + * @param requestRect the area relative to {@code scrollBounds} which describes the location of + * content to capture for the request + * @return the visible area within scrollBounds of the requested rectangle, return {@code null} + * in the case of an unrecoverable error condition, to abort the capture process + */ + Rect onScrollRequested(@NonNull V view, Rect scrollBounds, Rect requestRect); + + /** + * Restore the target after capture. + * <p> + * Put back anything that was changed in {@link #onPrepareForStart(View, Rect)}. + * + * @param view the view being captured + */ + void onPrepareForEnd(@NonNull V view); +} diff --git a/core/java/com/android/internal/view/ScrollCaptureViewSupport.java b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java new file mode 100644 index 000000000000..4087eda944e0 --- /dev/null +++ b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2020 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.internal.view; + +import android.graphics.HardwareRenderer; +import android.graphics.Matrix; +import android.graphics.RecordingCanvas; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.RenderNode; +import android.os.Handler; +import android.os.SystemClock; +import android.util.DisplayMetrics; +import android.view.ScrollCaptureCallback; +import android.view.ScrollCaptureSession; +import android.view.Surface; +import android.view.View; + +import java.lang.ref.WeakReference; +import java.util.function.Consumer; + +/** + * Provides a ScrollCaptureCallback implementation for to handle arbitrary View-based scrolling + * containers. + * <p> + * To use this class, supply the target view and an implementation of {@ScrollCaptureViewHelper} + * to the callback. + * + * @param <V> the specific View subclass handled + * @hide + */ +public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCallback { + + private final WeakReference<V> mWeakView; + private final ScrollCaptureViewHelper<V> mViewHelper; + private ViewRenderer mRenderer; + private Handler mUiHandler; + private boolean mStarted; + private boolean mEnded; + + static <V extends View> ScrollCaptureCallback createCallback(V view, + ScrollCaptureViewHelper<V> impl) { + return new ScrollCaptureViewSupport<>(view, impl); + } + + ScrollCaptureViewSupport(V containingView, ScrollCaptureViewHelper<V> viewHelper) { + mWeakView = new WeakReference<>(containingView); + mRenderer = new ViewRenderer(); + mUiHandler = containingView.getHandler(); + mViewHelper = viewHelper; + } + + // Base implementation of ScrollCaptureCallback + + @Override + public final void onScrollCaptureSearch(Consumer<Rect> onReady) { + V view = mWeakView.get(); + mStarted = false; + mEnded = false; + + if (view != null && view.isVisibleToUser() && mViewHelper.onAcceptSession(view)) { + onReady.accept(mViewHelper.onComputeScrollBounds(view)); + return; + } + onReady.accept(null); + } + + @Override + public final void onScrollCaptureStart(ScrollCaptureSession session, Runnable onReady) { + V view = mWeakView.get(); + mEnded = false; + mStarted = true; + + // Note: If somehow the view is already gone or detached, the first call to + // {@code onScrollCaptureImageRequest} will return an error and request the session to + // end. + if (view != null && view.isVisibleToUser()) { + mRenderer.setSurface(session.getSurface()); + mViewHelper.onPrepareForStart(view, session.getScrollBounds()); + } + onReady.run(); + } + + @Override + public final void onScrollCaptureImageRequest(ScrollCaptureSession session, Rect requestRect) { + V view = mWeakView.get(); + if (view == null || !view.isVisibleToUser()) { + // Signal to the controller that we have a problem and can't continue. + session.notifyBufferSent(0, null); + return; + } + Rect captureArea = mViewHelper.onScrollRequested(view, session.getScrollBounds(), + requestRect); + mRenderer.renderFrame(view, captureArea, mUiHandler, + () -> session.notifyBufferSent(0, captureArea)); + } + + @Override + public final void onScrollCaptureEnd(Runnable onReady) { + V view = mWeakView.get(); + if (mStarted && !mEnded) { + mViewHelper.onPrepareForEnd(view); + /* empty */ + mEnded = true; + mRenderer.trimMemory(); + mRenderer.setSurface(null); + } + onReady.run(); + } + + /** + * Internal helper class which assists in rendering sections of the view hierarchy relative to a + * given view. Used by framework implementations of ScrollCaptureHandler to render and dispatch + * image requests. + */ + static final class ViewRenderer { + // alpha, "reasonable default" from Javadoc + private static final float AMBIENT_SHADOW_ALPHA = 0.039f; + private static final float SPOT_SHADOW_ALPHA = 0.039f; + + // Default values: + // lightX = (screen.width() / 2) - windowLeft + // lightY = 0 - windowTop + // lightZ = 600dp + // lightRadius = 800dp + private static final float LIGHT_Z_DP = 400; + private static final float LIGHT_RADIUS_DP = 800; + private static final String TAG = "ViewRenderer"; + + private HardwareRenderer mRenderer; + private RenderNode mRootRenderNode; + private final RectF mTempRectF = new RectF(); + private final Rect mSourceRect = new Rect(); + private final Rect mTempRect = new Rect(); + private final Matrix mTempMatrix = new Matrix(); + private final int[] mTempLocation = new int[2]; + private long mLastRenderedSourceDrawingId = -1; + + + ViewRenderer() { + mRenderer = new HardwareRenderer(); + mRootRenderNode = new RenderNode("ScrollCaptureRoot"); + mRenderer.setContentRoot(mRootRenderNode); + + // TODO: Figure out a way to flip this on when we are sure the source window is opaque + mRenderer.setOpaque(false); + } + + public void setSurface(Surface surface) { + mRenderer.setSurface(surface); + } + + /** + * Cache invalidation check. If the source view is the same as the previous call (which is + * mostly always the case, then we can skip setting up lighting on each call (for now) + * + * @return true if the view changed, false if the view was previously rendered by this class + */ + private boolean updateForView(View source) { + if (mLastRenderedSourceDrawingId == source.getUniqueDrawingId()) { + return false; + } + mLastRenderedSourceDrawingId = source.getUniqueDrawingId(); + return true; + } + + // TODO: may need to adjust lightY based on the virtual canvas position to get + // consistent shadow positions across the whole capture. Or possibly just + // pull lightZ way back to make shadows more uniform. + private void setupLighting(View mSource) { + mLastRenderedSourceDrawingId = mSource.getUniqueDrawingId(); + DisplayMetrics metrics = mSource.getResources().getDisplayMetrics(); + mSource.getLocationOnScreen(mTempLocation); + final float lightX = metrics.widthPixels / 2f - mTempLocation[0]; + final float lightY = metrics.heightPixels - mTempLocation[1]; + final int lightZ = (int) (LIGHT_Z_DP * metrics.density); + final int lightRadius = (int) (LIGHT_RADIUS_DP * metrics.density); + + // Enable shadows for elevation/Z + mRenderer.setLightSourceGeometry(lightX, lightY, lightZ, lightRadius); + mRenderer.setLightSourceAlpha(AMBIENT_SHADOW_ALPHA, SPOT_SHADOW_ALPHA); + + } + + public void renderFrame(View localReference, Rect sourceRect, Handler handler, + Runnable onFrameCommitted) { + if (updateForView(localReference)) { + setupLighting(localReference); + } + buildRootDisplayList(localReference, sourceRect); + HardwareRenderer.FrameRenderRequest request = mRenderer.createRenderRequest(); + request.setVsyncTime(SystemClock.elapsedRealtimeNanos()); + request.setFrameCommitCallback(handler::post, onFrameCommitted); + request.setWaitForPresent(true); + request.syncAndDraw(); + } + + public void trimMemory() { + mRenderer.clearContent(); + } + + public void destroy() { + mRenderer.destroy(); + } + + private void transformToRoot(View local, Rect localRect, Rect outRect) { + mTempMatrix.reset(); + local.transformMatrixToGlobal(mTempMatrix); + mTempRectF.set(localRect); + mTempMatrix.mapRect(mTempRectF); + mTempRectF.round(outRect); + } + + private void buildRootDisplayList(View source, Rect localSourceRect) { + final View captureSource = source.getRootView(); + transformToRoot(source, localSourceRect, mTempRect); + mRootRenderNode.setPosition(0, 0, mTempRect.width(), mTempRect.height()); + RecordingCanvas canvas = mRootRenderNode.beginRecording(mTempRect.width(), + mTempRect.height()); + canvas.translate(-mTempRect.left, -mTempRect.top); + canvas.drawRenderNode(captureSource.updateDisplayListIfDirty()); + mRootRenderNode.endRecording(); + } + } +} diff --git a/core/java/com/android/internal/view/ScrollViewCaptureHelper.java b/core/java/com/android/internal/view/ScrollViewCaptureHelper.java new file mode 100644 index 000000000000..12bd461f810b --- /dev/null +++ b/core/java/com/android/internal/view/ScrollViewCaptureHelper.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2020 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.internal.view; + +import android.annotation.NonNull; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +/** + * ScrollCapture for ScrollView and <i>ScrollView-like</i> ViewGroups. + * <p> + * Requirements for proper operation: + * <ul> + * <li>contains at most 1 child. + * <li>scrolls to absolute positions with {@link View#scrollTo(int, int)}. + * <li>has a finite, known content height and scrolling range + * <li>correctly implements {@link View#canScrollVertically(int)} + * <li>correctly implements {@link ViewParent#requestChildRectangleOnScreen(View, + * Rect, boolean)} + * </ul> + */ +public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGroup> { + private int mStartScrollY; + private boolean mScrollBarEnabled; + private int mOverScrollMode; + + /** @see ScrollCaptureViewHelper#onPrepareForStart(View, Rect) */ + public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) { + mStartScrollY = view.getScrollY(); + mOverScrollMode = view.getOverScrollMode(); + if (mOverScrollMode != View.OVER_SCROLL_NEVER) { + view.setOverScrollMode(View.OVER_SCROLL_NEVER); + } + mScrollBarEnabled = view.isVerticalScrollBarEnabled(); + if (mScrollBarEnabled) { + view.setVerticalScrollBarEnabled(false); + } + } + + /** @see ScrollCaptureViewHelper#onScrollRequested(View, Rect, Rect) */ + public Rect onScrollRequested(@NonNull ViewGroup view, Rect scrollBounds, Rect requestRect) { + final View contentView = view.getChildAt(0); // returns null, does not throw IOOBE + if (contentView == null) { + return null; + } + /* + +---------+ <----+ Content [25,25 - 275,1025] (w=250,h=1000) + | | + ...|.........|... startScrollY=100 + | | + +--+---------+---+ <--+ Container View [0,0 - 300,300] (scrollY=200) + | . . | + --- | . +-----+ <------+ Scroll Bounds [50,50 - 250,250] (200x200) + ^ | . | | . | (Local to Container View, fixed/un-scrolled) + | | . | | . | + | | . | | . | + | | . +-----+ . | + | | . . | + | +--+---------+---+ + | | | + -+- | +-----+ | + | |#####| | <--+ Requested Bounds [0,300 - 200,400] (200x100) + | +-----+ | (Local to Scroll Bounds, fixed/un-scrolled) + | | + +---------+ + + Container View (ScrollView) [0,0 - 300,300] (scrollY = 200) + \__ Content [25,25 - 275,1025] (250x1000) (contentView) + \__ Scroll Bounds[50,50 - 250,250] (w=200,h=200) + \__ Requested Bounds[0,300 - 200,400] (200x100) + */ + + // 0) adjust the requestRect to account for scroll change since start + // + // Scroll Bounds[50,50 - 250,250] (w=200,h=200) + // \__ Requested Bounds[0,200 - 200,300] (200x100) + + // (y-100) (scrollY - mStartScrollY) + int scrollDelta = view.getScrollY() - mStartScrollY; + + // 1) Translate request rect to make it relative to container view + // + // Container View [0,0 - 300,300] (scrollY=200) + // \__ Requested Bounds[50,250 - 250,350] (w=250, h=100) + + // (x+50,y+50) + Rect requestedContainerBounds = new Rect(requestRect); + requestedContainerBounds.offset(0, -scrollDelta); + requestedContainerBounds.offset(scrollBounds.left, scrollBounds.top); + + // 2) Translate from container to contentView relative (applying container scrollY) + // + // Container View [0,0 - 300,300] (scrollY=200) + // \__ Content [25,25 - 275,1025] (250x1000) (contentView) + // \__ Requested Bounds[25,425 - 200,525] (w=250, h=100) + + // (x-25,y+175) (scrollY - content.top) + Rect requestedContentBounds = new Rect(requestedContainerBounds); + requestedContentBounds.offset( + view.getScrollX() - contentView.getLeft(), + view.getScrollY() - contentView.getTop()); + + + + // requestRect is now local to contentView as requestedContentBounds + // contentView (and each parent in turn if possible) will be scrolled + // (if necessary) to make all of requestedContent visible, (if possible!) + contentView.requestRectangleOnScreen(new Rect(requestedContentBounds), true); + + // update new offset between starting and current scroll position + scrollDelta = view.getScrollY() - mStartScrollY; + + + // TODO: adjust to avoid occlusions/minimize scroll changes + + Point offset = new Point(); + final Rect capturedRect = new Rect(requestedContentBounds); // empty + if (!view.getChildVisibleRect(contentView, capturedRect, offset)) { + capturedRect.setEmpty(); + return capturedRect; + } + // Transform back from global to content-view local + capturedRect.offset(-offset.x, -offset.y); + + // Then back to container view + capturedRect.offset( + contentView.getLeft() - view.getScrollX(), + contentView.getTop() - view.getScrollY()); + + + // And back to relative to scrollBounds + capturedRect.offset(-scrollBounds.left, -scrollBounds.top); + + // Apply scrollDelta again to return to make capturedRect relative to scrollBounds at + // the scroll position at start of capture. + capturedRect.offset(0, scrollDelta); + return capturedRect; + } + + /** @see ScrollCaptureViewHelper#onPrepareForEnd(View) */ + public void onPrepareForEnd(@NonNull ViewGroup view) { + view.scrollTo(0, mStartScrollY); + if (mOverScrollMode != View.OVER_SCROLL_NEVER) { + view.setOverScrollMode(mOverScrollMode); + } + if (mScrollBarEnabled) { + view.setVerticalScrollBarEnabled(true); + } + } +} diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index ba7aef7c208e..b51d4f509f38 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -1244,12 +1244,11 @@ void AndroidRuntime::exit(int code) { if (mExitWithoutCleanup) { ALOGI("VM exiting with result code %d, cleanup skipped.", code); - ::_exit(code); } else { ALOGI("VM exiting with result code %d.", code); onExit(code); - ::exit(code); } + ::_exit(code); } void AndroidRuntime::onVmCreated(JNIEnv* env) diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 6e8b9de2e732..ee25ac27b25c 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1111,12 +1111,13 @@ grants your app this permission. If you don't need this permission, be sure your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code targetSdkVersion}</a> is 4 or higher. - <p>Protection level: normal + <p>Protection level: dangerous --> <permission android:name="android.permission.READ_PHONE_STATE" + android:permissionGroup="android.permission-group.UNDEFINED" android:label="@string/permlab_readPhoneState" android:description="@string/permdesc_readPhoneState" - android:protectionLevel="normal" /> + android:protectionLevel="dangerous" /> <!-- Allows read access to the device's phone number(s). This is a subset of the capabilities granted by {@link #READ_PHONE_STATE} but is exposed to instant applications. diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 4065a6c83f8f..a8d1605c6fdd 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2527,6 +2527,21 @@ <flag name="noExcludeDescendants" value="0x8" /> </attr> + <!-- Hints the Android System whether the this View should be considered a scroll capture target. --> + <attr name="scrollCaptureHint"> + <!-- Let the Android System determine if the view can be a scroll capture target. --> + <flag name="auto" value="0" /> + <!-- Hint the Android System that this view is a likely target. If capable, it will + be ranked above other views without this flag. --> + <flag name="include" value="0x1" /> + <!-- Hint the Android System that this view should never be considered a scroll capture + target. --> + <flag name="exclude" value="0x2" /> + <!-- Hint the Android System that this view's children should not be examined and should + be excluded as a scroll capture target. --> + <flag name="excludeDescendants" value="0x4" /> + </attr> + <!-- Boolean that controls whether a view can take focus while in touch mode. If this is true for a view, that view can gain focus when clicked on, and can keep focus if another view is clicked on that doesn't have this attribute set to true. --> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 67d20da03925..fb887c338fc4 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -3020,6 +3020,8 @@ <public name="preserveLegacyExternalStorage" /> <public name="mimeGroup" /> <public name="gwpAsanMode" /> + <!-- @hide --> + <public name="scrollCaptureHint" /> </public-group> <public-group type="drawable" first-id="0x010800b5"> diff --git a/core/res/res/xml/default_zen_mode_config.xml b/core/res/res/xml/default_zen_mode_config.xml index 9110661536e2..873b9ebe4f1a 100644 --- a/core/res/res/xml/default_zen_mode_config.xml +++ b/core/res/res/xml/default_zen_mode_config.xml @@ -20,8 +20,8 @@ <!-- Default configuration for zen mode. See android.service.notification.ZenModeConfig. --> <zen version="9"> <allow alarms="true" media="true" system="false" calls="true" callsFrom="2" messages="false" - reminders="false" events="false" repeatCallers="true" conversations="true" - conversationsFrom="2"/> + reminders="false" events="false" repeatCallers="true" convos="false" + convosFrom="3"/> <automatic ruleId="EVENTS_DEFAULT_RULE" enabled="false" snoozing="false" name="Event" zen="1" component="android/com.android.server.notification.EventConditionProvider" conditionId="condition://android/event?userId=-10000&calendar=&reply=1"/> diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java index ea778fd6710a..a354f1d8109f 100644 --- a/core/tests/coretests/src/android/os/VibrationEffectTest.java +++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java @@ -22,9 +22,12 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.content.ContentInterface; +import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.net.Uri; @@ -37,6 +40,8 @@ import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class VibrationEffectTest { + private static final float SCALE_TOLERANCE = 1e-2f; + private static final String RINGTONE_URI_1 = "content://test/system/ringtone_1"; private static final String RINGTONE_URI_2 = "content://test/system/ringtone_2"; private static final String RINGTONE_URI_3 = "content://test/system/ringtone_3"; @@ -54,6 +59,12 @@ public class VibrationEffectTest { VibrationEffect.createOneShot(TEST_TIMING, VibrationEffect.DEFAULT_AMPLITUDE); private static final VibrationEffect TEST_WAVEFORM = VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1); + private static final VibrationEffect TEST_COMPOSED = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0, 100) + .compose(); @Test public void getRingtones_noPrebakedRingtones() { @@ -123,8 +134,14 @@ public class VibrationEffectTest { @Test public void testScaleWaveform() { - VibrationEffect.Waveform scaled = - ((VibrationEffect.Waveform) TEST_WAVEFORM).scale(1.1f, 200); + VibrationEffect.Waveform initial = (VibrationEffect.Waveform) TEST_WAVEFORM; + + VibrationEffect.Waveform copied = initial.scale(1f, 255); + assertEquals(255, copied.getAmplitudes()[0]); + assertEquals(0, copied.getAmplitudes()[1]); + assertEquals(-1, copied.getAmplitudes()[2]); + + VibrationEffect.Waveform scaled = initial.scale(1.1f, 200); assertEquals(200, scaled.getAmplitudes()[0]); assertEquals(0, scaled.getAmplitudes()[1]); } @@ -156,6 +173,66 @@ public class VibrationEffectTest { } } + @Test + public void testScaleComposed() { + VibrationEffect.Composed initial = (VibrationEffect.Composed) TEST_COMPOSED; + + VibrationEffect.Composed copied = initial.scale(1, 255); + assertEquals(1f, copied.getPrimitiveEffects().get(0).scale); + assertEquals(0.5f, copied.getPrimitiveEffects().get(1).scale); + assertEquals(0f, copied.getPrimitiveEffects().get(2).scale); + + VibrationEffect.Composed halved = initial.scale(1, 128); + assertEquals(0.5f, halved.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE); + assertEquals(0.25f, halved.getPrimitiveEffects().get(1).scale, SCALE_TOLERANCE); + assertEquals(0f, halved.getPrimitiveEffects().get(2).scale); + + VibrationEffect.Composed scaledUp = initial.scale(0.5f, 255); + assertEquals(1f, scaledUp.getPrimitiveEffects().get(0).scale); // does not scale up from 1 + assertTrue(0.5f < scaledUp.getPrimitiveEffects().get(1).scale); + assertEquals(0f, scaledUp.getPrimitiveEffects().get(2).scale); + + VibrationEffect.Composed restored = scaledUp.scale(2, 255); + assertEquals(1f, restored.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE); + assertEquals(0.5f, restored.getPrimitiveEffects().get(1).scale, SCALE_TOLERANCE); + assertEquals(0f, restored.getPrimitiveEffects().get(2).scale); + + VibrationEffect.Composed scaledDown = initial.scale(2, 255); + assertEquals(1f, scaledDown.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE); + assertTrue(0.5f > scaledDown.getPrimitiveEffects().get(1).scale); + assertEquals(0f, scaledDown.getPrimitiveEffects().get(2).scale, SCALE_TOLERANCE); + + VibrationEffect.Composed changeMax = initial.scale(1f, 51); + assertEquals(0.2f, changeMax.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE); + assertEquals(0.1f, changeMax.getPrimitiveEffects().get(1).scale, SCALE_TOLERANCE); + assertEquals(0f, changeMax.getPrimitiveEffects().get(2).scale); + } + + @Test + public void testScaleComposedFailsWhenMaxAmplitudeAboveThreshold() { + try { + ((VibrationEffect.Composed) TEST_COMPOSED).scale(1.1f, 1000); + fail("Max amplitude above threshold, should throw IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testScaleAppliesSameAdjustmentsOnAllEffects() { + VibrationEffect.OneShot oneShot = new VibrationEffect.OneShot(TEST_TIMING, TEST_AMPLITUDE); + VibrationEffect.Waveform waveform = new VibrationEffect.Waveform( + new long[] { TEST_TIMING }, new int[]{ TEST_AMPLITUDE }, -1); + VibrationEffect.Composed composed = + (VibrationEffect.Composed) VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, TEST_AMPLITUDE / 255f) + .compose(); + + assertEquals(oneShot.scale(2f, 128).getAmplitude(), + waveform.scale(2f, 128).getAmplitudes()[0]); + assertEquals(oneShot.scale(2f, 128).getAmplitude() / 255f, // convert amplitude to scale + composed.scale(2f, 128).getPrimitiveEffects().get(0).scale, + SCALE_TOLERANCE); + } private Resources mockRingtoneResources() { return mockRingtoneResources(new String[] { @@ -172,9 +249,22 @@ public class VibrationEffectTest { return mockResources; } - private Context mockContext(Resources r) { - Context ctx = mock(Context.class); - when(ctx.getResources()).thenReturn(r); - return ctx; + private Context mockContext(Resources resources) { + Context context = mock(Context.class); + ContentInterface contentInterface = mock(ContentInterface.class); + ContentResolver contentResolver = ContentResolver.wrap(contentInterface); + + try { + // ContentResolver#uncanonicalize is final, so we need to mock the ContentInterface it + // delegates the call to for the tests that require matching with the mocked URIs. + when(contentInterface.uncanonicalize(any())).then( + invocation -> invocation.getArgument(0)); + when(context.getContentResolver()).thenReturn(contentResolver); + when(context.getResources()).thenReturn(resources); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + + return context; } } diff --git a/core/tests/coretests/src/android/view/ScrollCaptureClientTest.java b/core/tests/coretests/src/android/view/ScrollCaptureClientTest.java new file mode 100644 index 000000000000..e6ac2d6c43da --- /dev/null +++ b/core/tests/coretests/src/android/view/ScrollCaptureClientTest.java @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.InstrumentationRegistry.getTargetContext; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; + +/** + * Tests of {@link ScrollCaptureClient}. + */ +@SuppressWarnings("UnnecessaryLocalVariable") +@RunWith(AndroidJUnit4.class) +public class ScrollCaptureClientTest { + + private final Point mPositionInWindow = new Point(1, 2); + private final Rect mLocalVisibleRect = new Rect(2, 3, 4, 5); + private final Rect mScrollBounds = new Rect(3, 4, 5, 6); + + private Handler mHandler; + private ScrollCaptureTarget mTarget1; + + @Mock + private Surface mSurface; + @Mock + private IScrollCaptureController mClientCallbacks; + @Mock + private View mMockView1; + @Mock + private ScrollCaptureCallback mCallback1; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mHandler = new Handler(getTargetContext().getMainLooper()); + + when(mMockView1.getHandler()).thenReturn(mHandler); + when(mMockView1.getScrollCaptureHint()).thenReturn(View.SCROLL_CAPTURE_HINT_INCLUDE); + + mTarget1 = new ScrollCaptureTarget( + mMockView1, mLocalVisibleRect, mPositionInWindow, mCallback1); + mTarget1.setScrollBounds(mScrollBounds); + } + + /** Test the DelayedAction timeout helper class works as expected. */ + @Test + public void testDelayedAction() { + Runnable action = Mockito.mock(Runnable.class); + ScrollCaptureClient.DelayedAction delayed = + new ScrollCaptureClient.DelayedAction(mHandler, 100, action); + try { + Thread.sleep(200); + } catch (InterruptedException ex) { + /* ignore */ + } + getInstrumentation().waitForIdleSync(); + assertFalse(delayed.cancel()); + assertFalse(delayed.timeoutNow()); + verify(action, times(1)).run(); + } + + /** Test the DelayedAction cancel() */ + @Test + public void testDelayedAction_cancel() { + Runnable action = Mockito.mock(Runnable.class); + ScrollCaptureClient.DelayedAction delayed = + new ScrollCaptureClient.DelayedAction(mHandler, 100, action); + try { + Thread.sleep(50); + } catch (InterruptedException ex) { + /* ignore */ + } + assertTrue(delayed.cancel()); + assertFalse(delayed.timeoutNow()); + try { + Thread.sleep(200); + } catch (InterruptedException ex) { + /* ignore */ + } + getInstrumentation().waitForIdleSync(); + verify(action, never()).run(); + } + + /** Test the DelayedAction timeoutNow() - for testing only */ + @Test + public void testDelayedAction_timeoutNow() { + Runnable action = Mockito.mock(Runnable.class); + ScrollCaptureClient.DelayedAction delayed = + new ScrollCaptureClient.DelayedAction(mHandler, 100, action); + try { + Thread.sleep(50); + } catch (InterruptedException ex) { + /* ignore */ + } + assertTrue(delayed.timeoutNow()); + assertFalse(delayed.cancel()); + getInstrumentation().waitForIdleSync(); + verify(action, times(1)).run(); + } + + /** Test creating a client with valid info */ + @Test + public void testConstruction() { + new ScrollCaptureClient(mTarget1, mClientCallbacks); + } + + /** Test creating a client fails if arguments are not valid. */ + @Test + public void testConstruction_requiresScrollBounds() { + try { + mTarget1.setScrollBounds(null); + new ScrollCaptureClient(mTarget1, mClientCallbacks); + fail("An exception was expected."); + } catch (RuntimeException ex) { + // Ignore, expected. + } + } + + @SuppressWarnings("SameParameterValue") + private static Answer<Void> runRunnable(int arg) { + return invocation -> { + Runnable r = invocation.getArgument(arg); + r.run(); + return null; + }; + } + + @SuppressWarnings("SameParameterValue") + private static Answer<Void> reportBufferSent(int sessionArg, long frameNum, Rect capturedArea) { + return invocation -> { + ScrollCaptureSession session = invocation.getArgument(sessionArg); + session.notifyBufferSent(frameNum, capturedArea); + return null; + }; + } + + /** @see ScrollCaptureClient#startCapture(Surface) */ + @Test + public void testStartCapture() throws Exception { + final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks); + + // Have the session start accepted immediately + doAnswer(runRunnable(1)).when(mCallback1) + .onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class)); + client.startCapture(mSurface); + getInstrumentation().waitForIdleSync(); + + verify(mCallback1, times(1)) + .onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class)); + verify(mClientCallbacks, times(1)).onCaptureStarted(); + verifyNoMoreInteractions(mClientCallbacks); + } + + @Test + public void testStartCaptureTimeout() throws Exception { + final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks); + client.startCapture(mSurface); + + // Force timeout to fire + client.getTimeoutAction().timeoutNow(); + + getInstrumentation().waitForIdleSync(); + verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class)); + } + + private void startClient(ScrollCaptureClient client) throws Exception { + doAnswer(runRunnable(1)).when(mCallback1) + .onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class)); + client.startCapture(mSurface); + getInstrumentation().waitForIdleSync(); + reset(mCallback1, mClientCallbacks); + } + + /** @see ScrollCaptureClient#requestImage(Rect) */ + @Test + public void testRequestImage() throws Exception { + final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks); + startClient(client); + + // Stub the callback to complete the request immediately + doAnswer(reportBufferSent(/* sessionArg */ 0, /* frameNum */ 1L, new Rect(1, 2, 3, 4))) + .when(mCallback1) + .onScrollCaptureImageRequest(any(ScrollCaptureSession.class), any(Rect.class)); + + // Make the inbound binder call + client.requestImage(new Rect(1, 2, 3, 4)); + + // Wait for handler thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mCallback1, times(1)).onScrollCaptureImageRequest( + any(ScrollCaptureSession.class), eq(new Rect(1, 2, 3, 4))); + + // Wait for binder thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mClientCallbacks, times(1)).onCaptureBufferSent(eq(1L), eq(new Rect(1, 2, 3, 4))); + + verifyNoMoreInteractions(mCallback1, mClientCallbacks); + } + + @Test + public void testRequestImageTimeout() throws Exception { + final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks); + startClient(client); + + // Make the inbound binder call + client.requestImage(new Rect(1, 2, 3, 4)); + + // Wait for handler thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mCallback1, times(1)).onScrollCaptureImageRequest( + any(ScrollCaptureSession.class), eq(new Rect(1, 2, 3, 4))); + + // Force timeout to fire + client.getTimeoutAction().timeoutNow(); + getInstrumentation().waitForIdleSync(); + + // (callback not stubbed, does nothing) + // Timeout triggers request to end capture + verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class)); + verifyNoMoreInteractions(mCallback1, mClientCallbacks); + } + + /** @see ScrollCaptureClient#endCapture() */ + @Test + public void testEndCapture() throws Exception { + final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks); + startClient(client); + + // Stub the callback to complete the request immediately + doAnswer(runRunnable(0)) + .when(mCallback1) + .onScrollCaptureEnd(any(Runnable.class)); + + // Make the inbound binder call + client.endCapture(); + + // Wait for handler thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class)); + + // Wait for binder thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mClientCallbacks, times(1)).onConnectionClosed(); + + verifyNoMoreInteractions(mCallback1, mClientCallbacks); + } + + @Test + public void testEndCaptureTimeout() throws Exception { + final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks); + startClient(client); + + // Make the inbound binder call + client.endCapture(); + + // Wait for handler thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class)); + + // Force timeout to fire + client.getTimeoutAction().timeoutNow(); + + // Wait for binder thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mClientCallbacks, times(1)).onConnectionClosed(); + + verifyNoMoreInteractions(mCallback1, mClientCallbacks); + } +} diff --git a/core/tests/coretests/src/android/view/ScrollCaptureTargetResolverTest.java b/core/tests/coretests/src/android/view/ScrollCaptureTargetResolverTest.java new file mode 100644 index 000000000000..8b21b8ecee89 --- /dev/null +++ b/core/tests/coretests/src/android/view/ScrollCaptureTargetResolverTest.java @@ -0,0 +1,498 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static androidx.test.InstrumentationRegistry.getTargetContext; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.LinkedList; +import java.util.function.Consumer; + +/** + * Tests of {@link ScrollCaptureTargetResolver}. + */ +@RunWith(AndroidJUnit4.class) +public class ScrollCaptureTargetResolverTest { + + private static final long TEST_TIMEOUT_MS = 2000; + private static final long RESOLVER_TIMEOUT_MS = 1000; + + private Handler mHandler; + private TargetConsumer mTargetConsumer; + + @Before + public void setUp() { + mTargetConsumer = new TargetConsumer(); + mHandler = new Handler(getTargetContext().getMainLooper()); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testEmptyQueue() throws InterruptedException { + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(new LinkedList<>()); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + // Test only + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertNull("Expected null due to empty queue", result); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testNoValidTargets() throws InterruptedException { + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + + // Supplies scrollBounds = null + FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback(); + callback1.setScrollBounds(null); + ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50), + new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO); + + // Supplies scrollBounds = empty rect + FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback(); + callback2.setScrollBounds(new Rect()); + ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50), + new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE); + + targetQueue.add(target1); + targetQueue.add(target2); + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + // Test only + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertNull("Expected null due to no valid targets", result); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testSingleTarget() throws InterruptedException { + FakeScrollCaptureCallback callback = new FakeScrollCaptureCallback(); + ScrollCaptureTarget target = createTarget(callback, + new Rect(20, 30, 40, 50), new Point(10, 10), + View.SCROLL_CAPTURE_HINT_AUTO); + callback.setScrollBounds(new Rect(2, 2, 18, 18)); + + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + targetQueue.add(target); + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + // Test only + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertSame("Excepted the same target as a result", target, result); + assertEquals("result has wrong scroll bounds", + new Rect(2, 2, 18, 18), result.getScrollBounds()); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testSingleTarget_backgroundThread() throws InterruptedException { + BackgroundTestCallback callback1 = new BackgroundTestCallback(); + ScrollCaptureTarget target1 = createTarget(callback1, + new Rect(20, 30, 40, 50), new Point(10, 10), + View.SCROLL_CAPTURE_HINT_AUTO); + callback1.setDelay(100); + callback1.setScrollBounds(new Rect(2, 2, 18, 18)); + + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + targetQueue.add(target1); + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + // Test only + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertSame("Excepted the single target1 as a result", target1, result); + assertEquals("Result has wrong scroll bounds", + new Rect(2, 2, 18, 18), result.getScrollBounds()); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testPreferNonEmptyBounds() throws InterruptedException { + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + + FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback(); + callback1.setScrollBounds(new Rect()); + ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50), + new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO); + + FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback(); + callback2.setScrollBounds(new Rect(0, 0, 20, 20)); + ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50), + new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE); + + FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback(); + callback3.setScrollBounds(null); + ScrollCaptureTarget target3 = createTarget(callback3, new Rect(20, 30, 40, 50), + new Point(0, 40), View.SCROLL_CAPTURE_HINT_AUTO); + + targetQueue.add(target1); + targetQueue.add(target2); // scrollBounds not null or empty() + targetQueue.add(target3); + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertEquals("Expected " + target2 + " as a result", target2, result); + assertEquals("result has wrong scroll bounds", + new Rect(0, 0, 20, 20), result.getScrollBounds()); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testPreferHintInclude() throws InterruptedException { + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + + FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback(); + callback1.setScrollBounds(new Rect(0, 0, 20, 20)); + ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50), + new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO); + + FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback(); + callback2.setScrollBounds(new Rect(1, 1, 19, 19)); + ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50), + new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE); + + FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback(); + callback3.setScrollBounds(new Rect(2, 2, 18, 18)); + ScrollCaptureTarget target3 = createTarget(callback3, new Rect(20, 30, 40, 50), + new Point(0, 40), View.SCROLL_CAPTURE_HINT_AUTO); + + targetQueue.add(target1); + targetQueue.add(target2); // * INCLUDE > AUTO + targetQueue.add(target3); + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertEquals("input = " + targetQueue + " Expected " + target2 + + " as the result, due to hint=INCLUDE", target2, result); + assertEquals("result has wrong scroll bounds", + new Rect(1, 1, 19, 19), result.getScrollBounds()); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testDescendantPreferred() throws InterruptedException { + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + + ViewGroup targetView1 = new FakeRootView(getTargetContext(), 0, 0, 60, 60); // 60x60 + ViewGroup targetView2 = new FakeRootView(getTargetContext(), 20, 30, 40, 50); // 20x20 + ViewGroup targetView3 = new FakeRootView(getTargetContext(), 5, 5, 15, 15); // 10x10 + + targetView1.addView(targetView2); + targetView2.addView(targetView3); + + // Create first target with an unrelated parent + FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback(); + callback1.setScrollBounds(new Rect(0, 0, 60, 60)); + ScrollCaptureTarget target1 = createTargetWithView(targetView1, callback1, + new Rect(0, 0, 60, 60), + new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO); + + // Create second target associated with a view within parent2 + FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback(); + callback2.setScrollBounds(new Rect(0, 0, 20, 20)); + ScrollCaptureTarget target2 = createTargetWithView(targetView2, callback2, + new Rect(0, 0, 20, 20), + new Point(20, 30), View.SCROLL_CAPTURE_HINT_AUTO); + + // Create third target associated with a view within parent3 + FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback(); + callback3.setScrollBounds(new Rect(0, 0, 15, 15)); + ScrollCaptureTarget target3 = createTargetWithView(targetView3, callback3, + new Rect(0, 0, 15, 15), + new Point(25, 35), View.SCROLL_CAPTURE_HINT_AUTO); + + targetQueue.add(target1); // auto, 60x60 + targetQueue.add(target2); // auto, 20x20 + targetQueue.add(target3); // auto, 15x15 <- innermost scrollable + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + // Test only + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertSame("Expected target3 as the result, due to relation", target3, result); + assertEquals("result has wrong scroll bounds", + new Rect(0, 0, 15, 15), result.getScrollBounds()); + } + + /** + * If a timeout expires, late results are ignored. + */ + @Test(timeout = TEST_TIMEOUT_MS) + public void testTimeout() throws InterruptedException { + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + + // callback 1, 10x10, hint=AUTO, responds immediately from bg thread + BackgroundTestCallback callback1 = new BackgroundTestCallback(); + callback1.setScrollBounds(new Rect(5, 5, 15, 15)); + ScrollCaptureTarget target1 = createTarget( + callback1, new Rect(20, 30, 40, 50), new Point(10, 10), + View.SCROLL_CAPTURE_HINT_AUTO); + targetQueue.add(target1); + + // callback 2, 20x20, hint=AUTO, responds after 5s from bg thread + BackgroundTestCallback callback2 = new BackgroundTestCallback(); + callback2.setScrollBounds(new Rect(0, 0, 20, 20)); + callback2.setDelay(5000); + ScrollCaptureTarget target2 = createTarget( + callback2, new Rect(20, 30, 40, 50), new Point(10, 10), + View.SCROLL_CAPTURE_HINT_AUTO); + targetQueue.add(target2); + + // callback 3, 20x20, hint=INCLUDE, responds after 10s from bg thread + BackgroundTestCallback callback3 = new BackgroundTestCallback(); + callback3.setScrollBounds(new Rect(0, 0, 20, 20)); + callback3.setDelay(10000); + ScrollCaptureTarget target3 = createTarget( + callback3, new Rect(20, 30, 40, 50), new Point(10, 10), + View.SCROLL_CAPTURE_HINT_INCLUDE); + targetQueue.add(target3); + + // callback 1 will be received + // callback 2 & 3 will be ignored due to timeout + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertSame("Expected target1 as the result, due to timeouts of others", target1, result); + assertEquals("result has wrong scroll bounds", + new Rect(5, 5, 15, 15), result.getScrollBounds()); + assertEquals("callback1 should have been called", + 1, callback1.getOnScrollCaptureSearchCount()); + assertEquals("callback2 should have been called", + 1, callback2.getOnScrollCaptureSearchCount()); + assertEquals("callback3 should have been called", + 1, callback3.getOnScrollCaptureSearchCount()); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testWithCallbackMultipleReplies() throws InterruptedException { + // Calls response methods 3 times each + RepeatingCaptureCallback callback1 = new RepeatingCaptureCallback(3); + callback1.setScrollBounds(new Rect(2, 2, 18, 18)); + ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50), + new Point(10, 10), View.SCROLL_CAPTURE_HINT_AUTO); + + FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback(); + callback2.setScrollBounds(new Rect(0, 0, 20, 20)); + ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50), + new Point(10, 10), View.SCROLL_CAPTURE_HINT_AUTO); + + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + targetQueue.add(target1); + targetQueue.add(target2); + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertSame("Expected target2 as the result, due to hint=INCLUDE", target2, result); + assertEquals("result has wrong scroll bounds", + new Rect(0, 0, 20, 20), result.getScrollBounds()); + assertEquals("callback1 should have been called once", + 1, callback1.getOnScrollCaptureSearchCount()); + assertEquals("callback2 should have been called once", + 1, callback2.getOnScrollCaptureSearchCount()); + } + + private static class TargetConsumer implements Consumer<ScrollCaptureTarget> { + volatile ScrollCaptureTarget mResult; + int mAcceptCount; + + ScrollCaptureTarget getLastValue() { + return mResult; + } + + int acceptCount() { + return mAcceptCount; + } + + @Override + public void accept(@Nullable ScrollCaptureTarget t) { + mAcceptCount++; + mResult = t; + } + } + + private void setupTargetView(View view, Rect localVisibleRect, int scrollCaptureHint) { + view.setScrollCaptureHint(scrollCaptureHint); + view.onVisibilityAggregated(true); + // Treat any offset as padding, outset localVisibleRect on all sides and use this as + // child bounds + Rect bounds = new Rect(localVisibleRect); + bounds.inset(-bounds.left, -bounds.top, bounds.left, bounds.top); + view.layout(bounds.left, bounds.top, bounds.right, bounds.bottom); + view.onVisibilityAggregated(true); + } + + private ScrollCaptureTarget createTarget(ScrollCaptureCallback callback, Rect localVisibleRect, + Point positionInWindow, int scrollCaptureHint) { + View mockView = new View(getTargetContext()); + return createTargetWithView(mockView, callback, localVisibleRect, positionInWindow, + scrollCaptureHint); + } + + private ScrollCaptureTarget createTargetWithView(View view, ScrollCaptureCallback callback, + Rect localVisibleRect, Point positionInWindow, int scrollCaptureHint) { + setupTargetView(view, localVisibleRect, scrollCaptureHint); + return new ScrollCaptureTarget(view, localVisibleRect, positionInWindow, callback); + } + + + static class FakeRootView extends ViewGroup implements ViewParent { + FakeRootView(Context context, int l, int t, int r, int b) { + super(context); + layout(l, t, r, b); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + } + } + + static class FakeScrollCaptureCallback implements ScrollCaptureCallback { + private Rect mScrollBounds; + private long mDelayMillis; + private int mOnScrollCaptureSearchCount; + + public int getOnScrollCaptureSearchCount() { + return mOnScrollCaptureSearchCount; + } + + @Override + public void onScrollCaptureSearch(Consumer<Rect> onReady) { + mOnScrollCaptureSearchCount++; + run(() -> { + Rect b = getScrollBounds(); + onReady.accept(b); + }); + } + + @Override + public void onScrollCaptureStart(ScrollCaptureSession session, Runnable onReady) { + run(onReady); + } + + @Override + public void onScrollCaptureImageRequest(ScrollCaptureSession session, Rect captureArea) { + run(() -> session.notifyBufferSent(0, captureArea)); + } + + @Override + public void onScrollCaptureEnd(Runnable onReady) { + run(onReady); + } + + public void setScrollBounds(@Nullable Rect scrollBounds) { + mScrollBounds = scrollBounds; + } + + public void setDelay(long delayMillis) { + mDelayMillis = delayMillis; + } + + protected Rect getScrollBounds() { + return mScrollBounds; + } + + protected void run(Runnable r) { + delay(); + r.run(); + } + + protected void delay() { + if (mDelayMillis > 0) { + try { + Thread.sleep(mDelayMillis); + } catch (InterruptedException e) { + // Ignore + } + } + } + } + + static class RepeatingCaptureCallback extends FakeScrollCaptureCallback { + private int mRepeatCount; + + RepeatingCaptureCallback(int repeatCount) { + mRepeatCount = repeatCount; + } + + protected void run(Runnable r) { + delay(); + for (int i = 0; i < mRepeatCount; i++) { + r.run(); + } + } + } + + /** Response to async calls on an arbitrary background thread */ + static class BackgroundTestCallback extends FakeScrollCaptureCallback { + static int sCount = 0; + private void runOnBackgroundThread(Runnable r) { + final Runnable target = () -> { + delay(); + r.run(); + }; + Thread t = new Thread(target); + synchronized (BackgroundTestCallback.this) { + sCount++; + } + t.setName("Background-Thread-" + sCount); + t.start(); + } + + @Override + protected void run(Runnable r) { + runOnBackgroundThread(r); + } + } +} diff --git a/core/tests/coretests/src/android/view/ViewGroupScrollCaptureTest.java b/core/tests/coretests/src/android/view/ViewGroupScrollCaptureTest.java new file mode 100644 index 000000000000..3af0533e763c --- /dev/null +++ b/core/tests/coretests/src/android/view/ViewGroupScrollCaptureTest.java @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.testng.AssertJUnit.assertSame; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.FlakyTest; +import androidx.test.filters.MediumTest; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * Exercises Scroll Capture search in {@link ViewGroup}. + */ +@Presubmit +@SmallTest +@FlakyTest(detail = "promote once confirmed flake-free") +@RunWith(MockitoJUnitRunner.class) +public class ViewGroupScrollCaptureTest { + + @Mock + ScrollCaptureCallback mMockCallback; + @Mock + ScrollCaptureCallback mMockCallback2; + + /** Make sure the hint flags are saved and loaded correctly. */ + @Test + public void testSetScrollCaptureHint() throws Exception { + final Context context = getInstrumentation().getContext(); + final MockViewGroup viewGroup = new MockViewGroup(context); + + assertNotNull(viewGroup); + assertEquals("Default scroll capture hint flags should be [SCROLL_CAPTURE_HINT_AUTO]", + ViewGroup.SCROLL_CAPTURE_HINT_AUTO, viewGroup.getScrollCaptureHint()); + + viewGroup.setScrollCaptureHint(View.SCROLL_CAPTURE_HINT_INCLUDE); + assertEquals("The scroll capture hint was not stored correctly.", + ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE, viewGroup.getScrollCaptureHint()); + + viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE); + assertEquals("The scroll capture hint was not stored correctly.", + ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE, viewGroup.getScrollCaptureHint()); + + viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS); + assertEquals("The scroll capture hint was not stored correctly.", + ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS, + viewGroup.getScrollCaptureHint()); + + viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE + | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS); + assertEquals("The scroll capture hint was not stored correctly.", + ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE + | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS, + viewGroup.getScrollCaptureHint()); + + viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE + | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS); + assertEquals("The scroll capture hint was not stored correctly.", + ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE + | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS, + viewGroup.getScrollCaptureHint()); + } + + /** + * Ensure a ViewGroup with 'scrollCaptureHint=auto', but no ScrollCaptureCallback set dispatches + * correctly. Verifies that the framework helper is called. Verifies a that non-null callback + * return results in an expected target in the results. + */ + @MediumTest + @Test + public void testDispatchScrollCaptureSearch_noCallback_hintAuto() throws Exception { + final Context context = getInstrumentation().getContext(); + final MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200); + + // When system internal scroll capture is requested, this callback is returned. + viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback); + + Rect localVisibleRect = new Rect(0, 0, 200, 200); + Point windowOffset = new Point(); + LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + + // Dispatch + viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList); + + // Verify the system checked for fallback support + viewGroup.assertDispatchScrollCaptureCount(1); + viewGroup.assertLastDispatchScrollCaptureArgs(localVisibleRect, windowOffset); + + // Verify the target is as expected. + assertEquals(1, targetList.size()); + ScrollCaptureTarget target = targetList.get(0); + assertSame("Target has the wrong callback", mMockCallback, target.getCallback()); + assertSame("Target has the wrong View", viewGroup, target.getContainingView()); + assertEquals("Target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO, + target.getContainingView().getScrollCaptureHint()); + } + + /** + * Ensure a ViewGroup with 'scrollCaptureHint=exclude' is ignored. The Framework helper is + * stubbed to return a callback. Verifies that the framework helper is not called (because of + * exclude), and no scroll capture target is added to the results. + */ + @MediumTest + @Test + public void testDispatchScrollCaptureSearch_noCallback_hintExclude() throws Exception { + final Context context = getInstrumentation().getContext(); + final MockViewGroup viewGroup = + new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_EXCLUDE); + + // When system internal scroll capture is requested, this callback is returned. + viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback); + + Rect localVisibleRect = new Rect(0, 0, 200, 200); + Point windowOffset = new Point(); + LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + + // Dispatch + viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList); + + // Verify the results. + assertEquals("Target list size should be zero.", 0, targetList.size()); + } + + /** + * Ensure that a ViewGroup with 'scrollCaptureHint=auto', and a scroll capture callback set + * dispatches as expected. Also verifies that the system fallback support is not called, and the + * the returned target is constructed correctly. + */ + @MediumTest + @Test + public void testDispatchScrollCaptureSearch_withCallback_hintAuto() throws Exception { + final Context context = getInstrumentation().getContext(); + MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200); + + // With an already provided scroll capture callback + viewGroup.setScrollCaptureCallback(mMockCallback); + + // When system internal scroll capture is requested, this callback is returned. + viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback); + + Rect localVisibleRect = new Rect(0, 0, 200, 200); + Point windowOffset = new Point(); + LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + + // Dispatch to the ViewGroup + viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList); + + // Confirm that framework support was not requested, + // because this view already had a callback set. + viewGroup.assertCreateScrollCaptureCallbackInternalCount(0); + + // Verify the target is as expected. + assertEquals(1, targetList.size()); + ScrollCaptureTarget target = targetList.get(0); + assertSame("Target has the wrong callback", mMockCallback, target.getCallback()); + assertSame("Target has the wrong View", viewGroup, target.getContainingView()); + assertEquals("Target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO, + target.getContainingView().getScrollCaptureHint()); + } + + /** + * Ensure a ViewGroup with a callback set, but 'scrollCaptureHint=exclude' is ignored. The + * exclude flag takes precedence. Verifies that the framework helper is not called (because of + * exclude, and a callback being set), and no scroll capture target is added to the results. + */ + @MediumTest + @Test + public void testDispatchScrollCaptureSearch_withCallback_hintExclude() throws Exception { + final Context context = getInstrumentation().getContext(); + MockViewGroup viewGroup = + new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_EXCLUDE); + // With an already provided scroll capture callback + viewGroup.setScrollCaptureCallback(mMockCallback); + + Rect localVisibleRect = new Rect(0, 0, 200, 200); + Point windowOffset = new Point(); + LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + + // Dispatch to the ViewGroup itself + viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList); + + // Confirm that framework support was not requested, because this view is excluded. + // (And because this view has a callback set.) + viewGroup.assertCreateScrollCaptureCallbackInternalCount(0); + + // Has callback, but hint=excluded, so excluded. + assertTrue(targetList.isEmpty()); + } + + /** + * Test scroll capture search dispatch to child views. + * <p> + * Verifies computation of child visible bounds. + * TODO: with scrollX / scrollY, split up into discrete tests + */ + @MediumTest + @Test + public void testDispatchScrollCaptureSearch_toChildren() throws Exception { + final Context context = getInstrumentation().getContext(); + final MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200); + + Rect localVisibleRect = new Rect(25, 50, 175, 150); + Point windowOffset = new Point(0, 0); + + // visible area + // |<- l=25, | + // | r=175 ->| + // +--------------------------+ + // | view1 (0, 0, 200, 25) | + // +---------------+----------+ + // | | | + // | view2 | view4 | --+ + // | (0, 25, | (inv) | | visible area + // | 150, 100)| | | + // +---------------+----------+ | t=50, b=150 + // | view3 | view5 | | + // | (0, 100 |(150, 100 | --+ + // | 200, 200) | 200, 200)| + // | | | + // | | | + // +---------------+----------+ (200,200) + + // View 1 is clipped and not visible. + final MockView view1 = new MockView(context, 0, 0, 200, 25); + viewGroup.addView(view1); + + // View 2 is partially visible. + final MockView view2 = new MockView(context, 0, 25, 150, 100); + viewGroup.addView(view2); + + // View 3 is partially visible. + // Pretend View3 can scroll by having framework provide fallback support + final MockView view3 = new MockView(context, 0, 100, 200, 200); + // When system internal scroll capture is requested for this view, return this callback. + view3.setScrollCaptureCallbackInternalForTest(mMockCallback); + viewGroup.addView(view3); + + // View 4 is invisible and should be ignored. + final MockView view4 = new MockView(context, 150, 25, 200, 100, View.INVISIBLE); + viewGroup.addView(view4); + + // View 4 is invisible and should be ignored. + final MockView view5 = new MockView(context, 150, 100, 200, 200); + // When system internal scroll capture is requested for this view, return this callback. + view5.setScrollCaptureCallback(mMockCallback2); + view5.setScrollCaptureHint(View.SCROLL_CAPTURE_HINT_INCLUDE); + viewGroup.addView(view5); + + // Where targets are added + final LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + + // Dispatch to the ViewGroup + viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList); + + // View 1 is entirely clipped by the parent and not visible, dispatch + // skips this view entirely. + view1.assertDispatchScrollCaptureSearchCount(0); + view1.assertCreateScrollCaptureCallbackInternalCount(0); + + // View 2, verify the computed localVisibleRect and windowOffset are correctly transformed + // to the child coordinate space + view2.assertDispatchScrollCaptureSearchCount(1); + view2.assertDispatchScrollCaptureSearchLastArgs( + new Rect(25, 25, 150, 75), new Point(0, 25)); + // No callback set, so the framework is asked for support + view2.assertCreateScrollCaptureCallbackInternalCount(1); + + // View 3, verify the computed localVisibleRect and windowOffset are correctly transformed + // to the child coordinate space + view3.assertDispatchScrollCaptureSearchCount(1); + view3.assertDispatchScrollCaptureSearchLastArgs( + new Rect(25, 0, 175, 50), new Point(0, 100)); + // No callback set, so the framework is asked for support + view3.assertCreateScrollCaptureCallbackInternalCount(1); + + // view4 is invisible, so it should be skipped entirely. + view4.assertDispatchScrollCaptureSearchCount(0); + view4.assertCreateScrollCaptureCallbackInternalCount(0); + + // view5 is partially visible + view5.assertDispatchScrollCaptureSearchCount(1); + view5.assertDispatchScrollCaptureSearchLastArgs( + new Rect(0, 0, 25, 50), new Point(150, 100)); + // view5 has a callback set on it, so internal framework support should not be consulted. + view5.assertCreateScrollCaptureCallbackInternalCount(0); + + // 2 views should have been returned, view3 & view5 + assertEquals(2, targetList.size()); + + ScrollCaptureTarget target = targetList.get(0); + assertSame("First target has the wrong View", view3, target.getContainingView()); + assertSame("First target has the wrong callback", mMockCallback, target.getCallback()); + assertEquals("First target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO, + target.getContainingView().getScrollCaptureHint()); + + target = targetList.get(1); + assertSame("Second target has the wrong View", view5, target.getContainingView()); + assertSame("Second target has the wrong callback", mMockCallback2, target.getCallback()); + assertEquals("Second target hint is incorrect", View.SCROLL_CAPTURE_HINT_INCLUDE, + target.getContainingView().getScrollCaptureHint()); + } + + public static final class MockView extends View { + private ScrollCaptureCallback mInternalCallback; + + private int mDispatchScrollCaptureSearchNumCalls; + private Rect mDispatchScrollCaptureSearchLastLocalVisibleRect; + private Point mDispatchScrollCaptureSearchLastWindowOffset; + private int mCreateScrollCaptureCallbackInternalCount; + + MockView(Context context) { + this(context, /* left */ 0, /* top */0, /* right */ 0, /* bottom */0); + } + + MockView(Context context, int left, int top, int right, int bottom) { + this(context, left, top, right, bottom, View.VISIBLE); + } + + MockView(Context context, int left, int top, int right, int bottom, int visibility) { + super(context); + setVisibility(visibility); + setFrame(left, top, right, bottom); + } + + public void setScrollCaptureCallbackInternalForTest(ScrollCaptureCallback internal) { + mInternalCallback = internal; + } + + void assertDispatchScrollCaptureSearchCount(int count) { + assertEquals("Unexpected number of calls to dispatchScrollCaptureSearch", + count, mDispatchScrollCaptureSearchNumCalls); + } + + void assertDispatchScrollCaptureSearchLastArgs(Rect localVisibleRect, Point windowOffset) { + assertEquals("arg localVisibleRect was incorrect.", + localVisibleRect, mDispatchScrollCaptureSearchLastLocalVisibleRect); + assertEquals("arg windowOffset was incorrect.", + windowOffset, mDispatchScrollCaptureSearchLastWindowOffset); + } + + void assertCreateScrollCaptureCallbackInternalCount(int count) { + assertEquals("Unexpected number of calls to createScrollCaptureCallackInternal", + count, mCreateScrollCaptureCallbackInternalCount); + } + + void reset() { + mDispatchScrollCaptureSearchNumCalls = 0; + mDispatchScrollCaptureSearchLastWindowOffset = null; + mDispatchScrollCaptureSearchLastLocalVisibleRect = null; + mCreateScrollCaptureCallbackInternalCount = 0; + + } + + @Override + public void dispatchScrollCaptureSearch(Rect localVisibleRect, Point windowOffset, + Queue<ScrollCaptureTarget> targets) { + mDispatchScrollCaptureSearchNumCalls++; + mDispatchScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect); + mDispatchScrollCaptureSearchLastWindowOffset = new Point(windowOffset); + super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets); + } + + @Override + @Nullable + public ScrollCaptureCallback createScrollCaptureCallbackInternal(Rect localVisibleRect, + Point offsetInWindow) { + mCreateScrollCaptureCallbackInternalCount++; + return mInternalCallback; + } + } + + public static final class MockViewGroup extends ViewGroup { + private ScrollCaptureCallback mInternalCallback; + private int mDispatchScrollCaptureSearchNumCalls; + private Rect mDispatchScrollCaptureSearchLastLocalVisibleRect; + private Point mDispatchScrollCaptureSearchLastWindowOffset; + private int mCreateScrollCaptureCallbackInternalCount; + + + MockViewGroup(Context context) { + this(context, /* left */ 0, /* top */0, /* right */ 0, /* bottom */0); + } + + MockViewGroup(Context context, int left, int top, int right, int bottom) { + this(context, left, top, right, bottom, View.SCROLL_CAPTURE_HINT_AUTO); + } + + MockViewGroup(Context context, int left, int top, int right, int bottom, + int scrollCaptureHint) { + super(context); + setScrollCaptureHint(scrollCaptureHint); + setFrame(left, top, right, bottom); + } + + public void setScrollCaptureCallbackInternalForTest(ScrollCaptureCallback internal) { + mInternalCallback = internal; + } + + void assertDispatchScrollCaptureSearchCount(int count) { + assertEquals("Unexpected number of calls to dispatchScrollCaptureSearch", + count, mDispatchScrollCaptureSearchNumCalls); + } + + @Override + @Nullable + public ScrollCaptureCallback createScrollCaptureCallbackInternal(Rect localVisibleRect, + Point offsetInWindow) { + mCreateScrollCaptureCallbackInternalCount++; + return mInternalCallback; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // We don't layout this view. + } + + void assertDispatchScrollCaptureCount(int count) { + assertEquals(count, mDispatchScrollCaptureSearchNumCalls); + } + + void assertLastDispatchScrollCaptureArgs(Rect localVisibleRect, Point windowOffset) { + assertEquals("arg localVisibleRect to dispatchScrollCaptureCallback was incorrect.", + localVisibleRect, mDispatchScrollCaptureSearchLastLocalVisibleRect); + assertEquals("arg windowOffset to dispatchScrollCaptureCallback was incorrect.", + windowOffset, mDispatchScrollCaptureSearchLastWindowOffset); + } + void assertCreateScrollCaptureCallbackInternalCount(int count) { + assertEquals("Unexpected number of calls to createScrollCaptureCallackInternal", + count, mCreateScrollCaptureCallbackInternalCount); + } + + void reset() { + mDispatchScrollCaptureSearchNumCalls = 0; + mDispatchScrollCaptureSearchLastWindowOffset = null; + mDispatchScrollCaptureSearchLastLocalVisibleRect = null; + mCreateScrollCaptureCallbackInternalCount = 0; + } + + @Override + public void dispatchScrollCaptureSearch(Rect localVisibleRect, Point windowOffset, + Queue<ScrollCaptureTarget> targets) { + mDispatchScrollCaptureSearchNumCalls++; + mDispatchScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect); + mDispatchScrollCaptureSearchLastWindowOffset = new Point(windowOffset); + super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets); + } + } +} diff --git a/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java b/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java index 769c57835662..5424b6f19038 100644 --- a/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java @@ -648,12 +648,6 @@ public class IntentForwarderActivityTest { } @Override - public void startActivity(Intent intent) { - mStartActivityIntent = intent; - mUserIdActivityLaunchedIn = getUserId(); - } - - @Override protected MetricsLogger getMetricsLogger() { return mMetricsLogger; } diff --git a/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java b/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java new file mode 100644 index 000000000000..63a68e99b788 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2020 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.internal.view; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import static androidx.test.InstrumentationRegistry.getContext; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.test.annotation.UiThreadTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Random; + +public class ScrollViewCaptureHelperTest { + + private FrameLayout mParent; + private ScrollView mTarget; + private LinearLayout mContent; + private WindowManager mWm; + + private WindowManager.LayoutParams mWindowLayoutParams; + + private static final int CHILD_VIEWS = 12; + public static final int CHILD_VIEW_HEIGHT = 300; + + private static final int WINDOW_WIDTH = 800; + private static final int WINDOW_HEIGHT = 1200; + + private static final int CAPTURE_HEIGHT = 600; + + private Random mRandom; + + private static float sDensity; + + @BeforeClass + public static void setUpClass() { + sDensity = getContext().getResources().getDisplayMetrics().density; + } + + @Before + @UiThreadTest + public void setUp() { + mRandom = new Random(); + mParent = new FrameLayout(getContext()); + + mTarget = new ScrollView(getContext()); + mParent.addView(mTarget, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + + mContent = new LinearLayout(getContext()); + mContent.setOrientation(LinearLayout.VERTICAL); + mTarget.addView(mContent, new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); + + for (int i = 0; i < CHILD_VIEWS; i++) { + TextView view = new TextView(getContext()); + view.setText("Child #" + i); + view.setTextColor(Color.WHITE); + view.setTextSize(30f); + view.setBackgroundColor(Color.rgb(mRandom.nextFloat(), mRandom.nextFloat(), + mRandom.nextFloat())); + mContent.addView(view, new ViewGroup.LayoutParams(MATCH_PARENT, CHILD_VIEW_HEIGHT)); + } + + // Window -> Parent -> Target -> Content + + mWm = getContext().getSystemService(WindowManager.class); + + // Setup the window that we are going to use + mWindowLayoutParams = new WindowManager.LayoutParams(WINDOW_WIDTH, WINDOW_HEIGHT, + TYPE_APPLICATION_OVERLAY, FLAG_NOT_TOUCHABLE, PixelFormat.OPAQUE); + mWindowLayoutParams.setTitle("ScrollViewCaptureHelper"); + mWindowLayoutParams.gravity = Gravity.CENTER; + mWm.addView(mParent, mWindowLayoutParams); + } + + @After + @UiThreadTest + public void tearDown() { + mWm.removeViewImmediate(mParent); + } + + @Test + @UiThreadTest + public void onPrepareForStart() { + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + } + + static void assertEmpty(Rect r) { + if (r != null && !r.isEmpty()) { + fail("Not true that " + r + " is empty"); + } + } + + static void assertContains(Rect parent, Rect child) { + if (!parent.contains(child)) { + fail("Not true that " + parent + " contains " + child); + } + } + + static void assertRectEquals(Rect parent, Rect child) { + if (!parent.equals(child)) { + fail("Not true that " + parent + " is equal to " + child); + } + } + + static Rect getVisibleRect(View v) { + Rect r = new Rect(0, 0, v.getWidth(), v.getHeight()); + v.getLocalVisibleRect(r); + return r; + } + + + static int assertScrollToY(View v, int scrollY) { + v.scrollTo(0, scrollY); + int dest = v.getScrollY(); + assertEquals(scrollY, dest); + return scrollY; + } + + + static void assertCapturedAreaCompletelyVisible(int startScrollY, Rect requestRect, + Rect localVisibleNow) { + Rect captured = new Rect(localVisibleNow); + captured.offset(0, -startScrollY); // make relative + + if (!captured.contains(requestRect)) { + fail("Not true that all of " + requestRect + " is contained by " + captured); + } + } + static void assertCapturedAreaPartiallyVisible(int startScrollY, Rect requestRect, + Rect localVisibleNow) { + Rect captured = new Rect(localVisibleNow); + captured.offset(0, -startScrollY); // make relative + + if (!Rect.intersects(captured, requestRect)) { + fail("Not true that any of " + requestRect + " intersects " + captured); + } + } + + @Test + @UiThreadTest + public void onScrollRequested_up_fromTop() { + final int startScrollY = assertScrollToY(mTarget, 0); + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + assertTrue(scrollBounds.height() > CAPTURE_HEIGHT); + + Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + + // The result is an empty rectangle and no scrolling, since it + // is not possible to physically scroll further up to make the + // requested area visible at all (it doesn't exist). + assertEmpty(result); + } + + @Test + @UiThreadTest + public void onScrollRequested_down_fromTop() { + final int startScrollY = assertScrollToY(mTarget, 0); + + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + assertTrue(scrollBounds.height() > CAPTURE_HEIGHT); + + // Capture between y = +1200 to +1500 pixels BELOW current top + Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(), + WINDOW_HEIGHT + CAPTURE_HEIGHT); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + assertRectEquals(request, result); + + assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent)); + } + + + @Test + @UiThreadTest + public void onScrollRequested_up_fromMiddle() { + final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT); + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0); + + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + + assertRectEquals(request, result); + + assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent)); + } + + @Test + @UiThreadTest + public void onScrollRequested_down_fromMiddle() { + final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT); + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(), + WINDOW_HEIGHT + CAPTURE_HEIGHT); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + assertRectEquals(request, result); + + assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent)); + } + + @Test + @UiThreadTest + public void onScrollRequested_up_fromBottom() { + final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2); + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + assertRectEquals(request, result); + + assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent)); + } + + @Test + @UiThreadTest + public void onScrollRequested_down_fromBottom() { + final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2); + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(), + WINDOW_HEIGHT + CAPTURE_HEIGHT); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + + // The result is an empty rectangle and no scrolling, since it + // is not possible to physically scroll further down to make the + // requested area visible at all (it doesn't exist). + assertEmpty(result); + } + + @Test + @UiThreadTest + public void onScrollRequested_offTopEdge() { + final int startScrollY = assertScrollToY(mTarget, 0); + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + // Create a request which lands halfway off the top of the content + //from -1500 to -900, (starting at 1200 = -300 to +300 within the content) + int top = 0; + Rect request = new Rect( + 0, top - (CAPTURE_HEIGHT / 2), + scrollBounds.width(), top + (CAPTURE_HEIGHT / 2)); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + // The result is a partial result + Rect expectedResult = new Rect(request); + expectedResult.top += 300; // top half clipped + assertRectEquals(expectedResult, result); + assertCapturedAreaPartiallyVisible(startScrollY, request, getVisibleRect(mContent)); + } + + @Test + @UiThreadTest + public void onScrollRequested_offBottomEdge() { + final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2); // 2400 + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + // Create a request which lands halfway off the bottom of the content + //from 600 to to 1200, (starting at 2400 = 3000 to 3600 within the content) + + int bottom = WINDOW_HEIGHT; + Rect request = new Rect( + 0, bottom - (CAPTURE_HEIGHT / 2), + scrollBounds.width(), bottom + (CAPTURE_HEIGHT / 2)); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + + Rect expectedResult = new Rect(request); + expectedResult.bottom -= 300; // bottom half clipped + assertRectEquals(expectedResult, result); + assertCapturedAreaPartiallyVisible(startScrollY, request, getVisibleRect(mContent)); + + } + + @Test + @UiThreadTest + public void onPrepareForEnd() { + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + svc.onPrepareForEnd(mTarget); + } +} diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 18086ec0313e..c5ac451a9539 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -283,6 +283,12 @@ "group": "WM_DEBUG_APP_TRANSITIONS", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-1517908912": { + "message": "requestScrollCapture: caught exception dispatching to window.token=%s", + "level": "WARN", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "-1515151503": { "message": ">>> OPEN TRANSACTION removeReplacedWindows", "level": "INFO", @@ -1441,6 +1447,12 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimation.java" }, + "646981048": { + "message": "Invalid displayId for requestScrollCapture: %d", + "level": "ERROR", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "662572728": { "message": "Attempted to add a toast window with bad token %s. Aborting.", "level": "WARN", @@ -1597,6 +1609,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "1046922686": { + "message": "requestScrollCapture: caught exception dispatching callback: %s", + "level": "WARN", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "1051545910": { "message": "Exit animation finished in %s: remove=%b", "level": "VERBOSE", diff --git a/data/keyboards/Vendor_18d1_Product_0200.kcm b/data/keyboards/Vendor_18d1_Product_0200.kcm new file mode 100644 index 000000000000..231fac6b48b7 --- /dev/null +++ b/data/keyboards/Vendor_18d1_Product_0200.kcm @@ -0,0 +1,48 @@ +# Copyright (C) 2020 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. + +type FULL + +key BUTTON_A { + base: fallback DPAD_CENTER +} + +key BUTTON_B { + base: fallback BACK +} + +key BUTTON_X { + base: fallback DPAD_CENTER +} + +key BUTTON_Y { + base: fallback BACK +} + +key BUTTON_THUMBL { + base: fallback DPAD_CENTER +} + +key BUTTON_THUMBR { + base: fallback DPAD_CENTER +} + +key BUTTON_SELECT { + base: fallback MENU +} + +key BUTTON_MODE { + base: fallback MENU +} + diff --git a/data/keyboards/Vendor_18d1_Product_0200.kl b/data/keyboards/Vendor_18d1_Product_0200.kl new file mode 100644 index 000000000000..d30bcc60e663 --- /dev/null +++ b/data/keyboards/Vendor_18d1_Product_0200.kl @@ -0,0 +1,71 @@ +# Copyright (C) 2020 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. + +# +# Keyboard map for the android virtual remote running as a gamepad +# + +key 0x130 BUTTON_A +key 0x131 BUTTON_B +key 0x133 BUTTON_X +key 0x134 BUTTON_Y + +key 0x136 BUTTON_L2 +key 0x137 BUTTON_R2 +key 0x138 BUTTON_L1 +key 0x139 BUTTON_R1 + +key 0x13a BUTTON_SELECT +key 0x13b BUTTON_START +key 0x13c BUTTON_MODE + +key 0x13d BUTTON_THUMBL +key 0x13e BUTTON_THUMBR + +key 103 DPAD_UP +key 108 DPAD_DOWN +key 105 DPAD_LEFT +key 106 DPAD_RIGHT + +# Generic usage buttons +key 0x2c0 BUTTON_1 +key 0x2c1 BUTTON_2 +key 0x2c2 BUTTON_3 +key 0x2c3 BUTTON_4 +key 0x2c4 BUTTON_5 +key 0x2c5 BUTTON_6 +key 0x2c6 BUTTON_7 +key 0x2c7 BUTTON_8 +key 0x2c8 BUTTON_9 +key 0x2c9 BUTTON_10 +key 0x2ca BUTTON_11 +key 0x2cb BUTTON_12 +key 0x2cc BUTTON_13 +key 0x2cd BUTTON_14 +key 0x2ce BUTTON_15 +key 0x2cf BUTTON_16 + +# assistant buttons +key 0x246 VOICE_ASSIST +key 0x247 ASSIST + +axis 0x00 X +axis 0x01 Y +axis 0x02 Z +axis 0x05 RZ +axis 0x09 RTRIGGER +axis 0x0a LTRIGGER +axis 0x10 HAT_X +axis 0x11 HAT_Y + diff --git a/graphics/java/android/graphics/SurfaceTexture.java b/graphics/java/android/graphics/SurfaceTexture.java index cd878c5a77cb..228d03a1dd10 100644 --- a/graphics/java/android/graphics/SurfaceTexture.java +++ b/graphics/java/android/graphics/SurfaceTexture.java @@ -120,7 +120,7 @@ public class SurfaceTexture { /** * Construct a new SurfaceTexture to stream images to a given OpenGL texture. - * + * <p> * In single buffered mode the application is responsible for serializing access to the image * content buffer. Each time the image content is to be updated, the * {@link #releaseTexImage()} method must be called before the image content producer takes @@ -143,7 +143,7 @@ public class SurfaceTexture { /** * Construct a new SurfaceTexture to stream images to a given OpenGL texture. - * + * <p> * In single buffered mode the application is responsible for serializing access to the image * content buffer. Each time the image content is to be updated, the * {@link #releaseTexImage()} method must be called before the image content producer takes @@ -152,7 +152,7 @@ public class SurfaceTexture { * must be called before each ANativeWindow_lock, or that call will fail. When producing * image content with OpenGL ES, {@link #releaseTexImage()} must be called before the first * OpenGL ES function call each frame. - * + * <p> * Unlike {@link #SurfaceTexture(int, boolean)}, which takes an OpenGL texture object name, * this constructor creates the SurfaceTexture in detached mode. A texture name must be passed * in using {@link #attachToGLContext} before calling {@link #releaseTexImage()} and producing @@ -222,15 +222,15 @@ public class SurfaceTexture { * method. Both video and camera based image producers do override the size. This method may * be used to set the image size when producing images with {@link android.graphics.Canvas} (via * {@link android.view.Surface#lockCanvas}), or OpenGL ES (via an EGLSurface). - * + * <p> * The new default buffer size will take effect the next time the image producer requests a * buffer to fill. For {@link android.graphics.Canvas} this will be the next time {@link * android.view.Surface#lockCanvas} is called. For OpenGL ES, the EGLSurface should be * destroyed (via eglDestroySurface), made not-current (via eglMakeCurrent), and then recreated - * (via eglCreateWindowSurface) to ensure that the new default size has taken effect. - * + * (via {@code eglCreateWindowSurface}) to ensure that the new default size has taken effect. + * <p> * The width and height parameters must be no greater than the minimum of - * GL_MAX_VIEWPORT_DIMS and GL_MAX_TEXTURE_SIZE (see + * {@code GL_MAX_VIEWPORT_DIMS} and {@code GL_MAX_TEXTURE_SIZE} (see * {@link javax.microedition.khronos.opengles.GL10#glGetIntegerv glGetIntegerv}). * An error due to invalid dimensions might not be reported until * updateTexImage() is called. @@ -242,7 +242,7 @@ public class SurfaceTexture { /** * Update the texture image to the most recent frame from the image stream. This may only be * called while the OpenGL ES context that owns the texture is current on the calling thread. - * It will implicitly bind its texture to the GL_TEXTURE_EXTERNAL_OES texture target. + * It will implicitly bind its texture to the {@code GL_TEXTURE_EXTERNAL_OES} texture target. */ public void updateTexImage() { nativeUpdateTexImage(); @@ -251,6 +251,7 @@ public class SurfaceTexture { /** * Releases the the texture content. This is needed in single buffered mode to allow the image * content producer to take ownership of the image buffer. + * <p> * For more information see {@link #SurfaceTexture(int, boolean)}. */ public void releaseTexImage() { @@ -263,7 +264,7 @@ public class SurfaceTexture { * ES texture object will be deleted as a result of this call. After calling this method all * calls to {@link #updateTexImage} will throw an {@link java.lang.IllegalStateException} until * a successful call to {@link #attachToGLContext} is made. - * + * <p> * This can be used to access the SurfaceTexture image contents from multiple OpenGL ES * contexts. Note, however, that the image contents are only accessible from one OpenGL ES * context at a time. @@ -279,8 +280,8 @@ public class SurfaceTexture { * Attach the SurfaceTexture to the OpenGL ES context that is current on the calling thread. A * new OpenGL ES texture object is created and populated with the SurfaceTexture image frame * that was current at the time of the last call to {@link #detachFromGLContext}. This new - * texture is bound to the GL_TEXTURE_EXTERNAL_OES texture target. - * + * texture is bound to the {@code GL_TEXTURE_EXTERNAL_OES} texture target. + * <p> * This can be used to access the SurfaceTexture image contents from multiple OpenGL ES * contexts. Note, however, that the image contents are only accessible from one OpenGL ES * context at a time. @@ -297,16 +298,16 @@ public class SurfaceTexture { /** * Retrieve the 4x4 texture coordinate transform matrix associated with the texture image set by - * the most recent call to updateTexImage. - * + * the most recent call to {@link #updateTexImage}. + * <p> * This transform matrix maps 2D homogeneous texture coordinates of the form (s, t, 0, 1) with s * and t in the inclusive range [0, 1] to the texture coordinate that should be used to sample * that location from the texture. Sampling the texture outside of the range of this transform * is undefined. - * + * <p> * The matrix is stored in column-major order so that it may be passed directly to OpenGL ES via - * the glLoadMatrixf or glUniformMatrix4fv functions. - * + * the {@code glLoadMatrixf} or {@code glUniformMatrix4fv} functions. + * <p> * If the underlying buffer has a crop associated with it, the transformation will also include * a slight scale to cut off a 1-texel border around the edge of the crop. This ensures that * when the texture is bilinear sampled that no texels outside of the buffer's valid region @@ -326,7 +327,7 @@ public class SurfaceTexture { /** * Retrieve the timestamp associated with the texture image set by the most recent call to - * updateTexImage. + * {@link #updateTexImage}. * * <p>This timestamp is in nanoseconds, and is normally monotonically increasing. The timestamp * should be unaffected by time-of-day adjustments. The specific meaning and zero point of the @@ -337,8 +338,8 @@ public class SurfaceTexture { * * <p>For camera sources, timestamps should be strictly monotonic. Timestamps from MediaPlayer * sources may be reset when the playback position is set. For EGL and Vulkan producers, the - * timestamp is the desired present time set with the EGL_ANDROID_presentation_time or - * VK_GOOGLE_display_timing extensions.</p> + * timestamp is the desired present time set with the {@code EGL_ANDROID_presentation_time} or + * {@code VK_GOOGLE_display_timing} extensions.</p> */ public long getTimestamp() { @@ -346,16 +347,17 @@ public class SurfaceTexture { } /** - * release() frees all the buffers and puts the SurfaceTexture into the + * {@code release()} frees all the buffers and puts the SurfaceTexture into the * 'abandoned' state. Once put in this state the SurfaceTexture can never * leave it. When in the 'abandoned' state, all methods of the - * IGraphicBufferProducer interface will fail with the NO_INIT error. - * + * {@code IGraphicBufferProducer} interface will fail with the {@code NO_INIT} + * error. + * <p> * Note that while calling this method causes all the buffers to be freed * from the perspective of the the SurfaceTexture, if there are additional * references on the buffers (e.g. if a buffer is referenced by a client or * by OpenGL ES as a texture) then those buffer will remain allocated. - * + * <p> * Always call this method when you are done with SurfaceTexture. Failing * to do so may delay resource deallocation for a significant amount of * time. @@ -367,7 +369,7 @@ public class SurfaceTexture { } /** - * Returns true if the SurfaceTexture was released. + * Returns {@code true} if the SurfaceTexture was released. * * @see #release() */ @@ -400,7 +402,7 @@ public class SurfaceTexture { } /** - * Returns true if the SurfaceTexture is single-buffered + * Returns {@code true} if the SurfaceTexture is single-buffered. * @hide */ public boolean isSingleBuffered() { diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index cbf2364b50a4..1bfa9991e5d2 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -1312,7 +1312,7 @@ public final class MediaFormat { } /** - * Returns the value of an long key, or the default value if the key is missing. + * Returns the value of a long key, or the default value if the key is missing. * * @return defaultValue if the key does not exist or the stored value for the key is null * @throws ClassCastException if the stored value for the key is int, float, ByteBuffer or @@ -1340,19 +1340,15 @@ public final class MediaFormat { } /** - * Returns the value of an float key, or the default value if the key is missing. + * Returns the value of a float key, or the default value if the key is missing. * * @return defaultValue if the key does not exist or the stored value for the key is null * @throws ClassCastException if the stored value for the key is int, long, ByteBuffer or * String */ public final float getFloat(@NonNull String name, float defaultValue) { - try { - return getFloat(name); - } catch (NullPointerException e) { - /* no such field or field is null */ - return defaultValue; - } + Object value = mMap.get(name); + return value != null ? (float) value : defaultValue; } /** @@ -1366,7 +1362,7 @@ public final class MediaFormat { } /** - * Returns the value of an string key, or the default value if the key is missing. + * Returns the value of a string key, or the default value if the key is missing. * * @return defaultValue if the key does not exist or the stored value for the key is null * @throws ClassCastException if the stored value for the key is int, long, float or ByteBuffer diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index bd8fb9602656..cfe6db9fe3cf 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -237,9 +237,9 @@ public final class MediaRouter2 { } catch (RemoteException ex) { Log.e(TAG, "Unable to unregister media router.", ex); } + mStub = null; } mShouldUpdateRoutes = true; - mStub = null; } } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index b694fd059bfa..3b570b60ff24 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -147,14 +147,16 @@ public final class MediaRouter2Manager { } synchronized (sLock) { - if (mCallbackRecords.size() == 0 && mClient != null) { - try { - mMediaRouterService.unregisterManager(mClient); - } catch (RemoteException ex) { - Log.e(TAG, "Unable to unregister media router manager", ex); + if (mCallbackRecords.size() == 0) { + if (mClient != null) { + try { + mMediaRouterService.unregisterManager(mClient); + } catch (RemoteException ex) { + Log.e(TAG, "Unable to unregister media router manager", ex); + } + mClient = null; } - //TODO: clear mRoutes? - mClient = null; + mRoutes.clear(); mPreferredFeaturesMap.clear(); } } diff --git a/media/java/android/media/tv/ITvRemoteServiceInput.aidl b/media/java/android/media/tv/ITvRemoteServiceInput.aidl index a0b6c9bfc8d8..0e6563a1ab13 100644 --- a/media/java/android/media/tv/ITvRemoteServiceInput.aidl +++ b/media/java/android/media/tv/ITvRemoteServiceInput.aidl @@ -39,4 +39,10 @@ oneway interface ITvRemoteServiceInput { void sendPointerUp(IBinder token, int pointerId); @UnsupportedAppUsage void sendPointerSync(IBinder token); -}
\ No newline at end of file + + // API specific to gamepads. Close gamepads with closeInputBridge + void openGamepadBridge(IBinder token, String name); + void sendGamepadKeyDown(IBinder token, int keyCode); + void sendGamepadKeyUp(IBinder token, int keyCode); + void sendGamepadAxisValue(IBinder token, int axis, float value); +} diff --git a/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java b/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java index 0bf0f97d2c5e..b97ac26bb915 100644 --- a/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java +++ b/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java @@ -16,6 +16,8 @@ package com.android.media.tv.remoteprovider; +import android.annotation.FloatRange; +import android.annotation.NonNull; import android.content.Context; import android.media.tv.ITvRemoteProvider; import android.media.tv.ITvRemoteServiceInput; @@ -24,6 +26,7 @@ import android.os.RemoteException; import android.util.Log; import java.util.LinkedList; +import java.util.Objects; /** * Base class for emote providers implemented in unbundled service. @@ -124,27 +127,75 @@ public abstract class TvRemoteProvider { * @param maxPointers Maximum supported pointers * @throws RuntimeException */ - public void openRemoteInputBridge(IBinder token, String name, int width, int height, - int maxPointers) throws RuntimeException { + public void openRemoteInputBridge( + IBinder token, String name, int width, int height, int maxPointers) + throws RuntimeException { + final IBinder finalToken = Objects.requireNonNull(token); + final String finalName = Objects.requireNonNull(name); + synchronized (mOpenBridgeRunnables) { if (mRemoteServiceInput == null) { - Log.d(TAG, "Delaying openRemoteInputBridge() for " + name); + Log.d(TAG, "Delaying openRemoteInputBridge() for " + finalName); mOpenBridgeRunnables.add(() -> { try { mRemoteServiceInput.openInputBridge( - token, name, width, height, maxPointers); - Log.d(TAG, "Delayed openRemoteInputBridge() for " + name + ": success"); + finalToken, finalName, width, height, maxPointers); + Log.d(TAG, "Delayed openRemoteInputBridge() for " + finalName + + ": success"); + } catch (RemoteException re) { + Log.e(TAG, "Delayed openRemoteInputBridge() for " + finalName + + ": failure", re); + } + }); + return; + } + } + try { + mRemoteServiceInput.openInputBridge(finalToken, finalName, width, height, maxPointers); + Log.d(TAG, "openRemoteInputBridge() for " + finalName + ": success"); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Opens an input bridge as a gamepad device. + * Clients should pass in a token that can be used to match this request with a token that + * will be returned by {@link TvRemoteProvider#onInputBridgeConnected(IBinder token)} + * <p> + * The token should be used for subsequent calls. + * </p> + * + * @param token Identifier for this connection + * @param name Device name + * @throws RuntimeException + * + * @hide + */ + public void openGamepadBridge(@NonNull IBinder token, @NonNull String name) + throws RuntimeException { + final IBinder finalToken = Objects.requireNonNull(token); + final String finalName = Objects.requireNonNull(name); + synchronized (mOpenBridgeRunnables) { + if (mRemoteServiceInput == null) { + Log.d(TAG, "Delaying openGamepadBridge() for " + finalName); + + mOpenBridgeRunnables.add(() -> { + try { + mRemoteServiceInput.openGamepadBridge(finalToken, finalName); + Log.d(TAG, "Delayed openGamepadBridge() for " + finalName + ": success"); } catch (RemoteException re) { - Log.e(TAG, "Delayed openRemoteInputBridge() for " + name + ": failure", re); + Log.e(TAG, "Delayed openGamepadBridge() for " + finalName + ": failure", + re); } }); return; } } try { - mRemoteServiceInput.openInputBridge(token, name, width, height, maxPointers); - Log.d(TAG, "openRemoteInputBridge() for " + name + ": success"); + mRemoteServiceInput.openGamepadBridge(token, finalName); + Log.d(TAG, "openGamepadBridge() for " + finalName + ": success"); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } @@ -157,6 +208,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void closeInputBridge(IBinder token) throws RuntimeException { + Objects.requireNonNull(token); try { mRemoteServiceInput.closeInputBridge(token); } catch (RemoteException re) { @@ -173,6 +225,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void clearInputBridge(IBinder token) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "clearInputBridge() token " + token); try { mRemoteServiceInput.clearInputBridge(token); @@ -190,6 +243,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void sendTimestamp(IBinder token, long timestamp) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "sendTimestamp() token: " + token + ", timestamp: " + timestamp); try { @@ -207,6 +261,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void sendKeyUp(IBinder token, int keyCode) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "sendKeyUp() token: " + token + ", keyCode: " + keyCode); try { mRemoteServiceInput.sendKeyUp(token, keyCode); @@ -223,6 +278,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void sendKeyDown(IBinder token, int keyCode) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "sendKeyDown() token: " + token + ", keyCode: " + keyCode); try { @@ -241,6 +297,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void sendPointerUp(IBinder token, int pointerId) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "sendPointerUp() token: " + token + ", pointerId: " + pointerId); try { @@ -262,6 +319,7 @@ public abstract class TvRemoteProvider { */ public void sendPointerDown(IBinder token, int pointerId, int x, int y) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "sendPointerDown() token: " + token + ", pointerId: " + pointerId); try { @@ -278,6 +336,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void sendPointerSync(IBinder token) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "sendPointerSync() token: " + token); try { mRemoteServiceInput.sendPointerSync(token); @@ -286,6 +345,94 @@ public abstract class TvRemoteProvider { } } + /** + * Send a notification that a gamepad key was pressed. + * + * Supported buttons are: + * <ul> + * <li> Right-side buttons: BUTTON_A, BUTTON_B, BUTTON_X, BUTTON_Y + * <li> Digital Triggers and bumpers: BUTTON_L1, BUTTON_R1, BUTTON_L2, BUTTON_R2 + * <li> Thumb buttons: BUTTON_THUMBL, BUTTON_THUMBR + * <li> DPad buttons: DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT + * <li> Gamepad buttons: BUTTON_SELECT, BUTTON_START, BUTTON_MODE + * <li> Generic buttons: BUTTON_1, BUTTON_2, ...., BUTTON16 + * <li> Assistant: ASSIST, VOICE_ASSIST + * </ul> + * + * @param token identifier for the device + * @param keyCode the gamepad key that was pressed (like BUTTON_A) + * + * @hide + */ + public void sendGamepadKeyDown(@NonNull IBinder token, int keyCode) throws RuntimeException { + Objects.requireNonNull(token); + if (DEBUG_KEYS) { + Log.d(TAG, "sendGamepadKeyDown() token: " + token); + } + + try { + mRemoteServiceInput.sendGamepadKeyDown(token, keyCode); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Send a notification that a gamepad key was released. + * + * @see sendGamepadKeyDown for supported key codes. + * + * @param token identifier for the device + * @param keyCode the gamepad key that was pressed + * + * @hide + */ + public void sendGamepadKeyUp(@NonNull IBinder token, int keyCode) throws RuntimeException { + Objects.requireNonNull(token); + if (DEBUG_KEYS) { + Log.d(TAG, "sendGamepadKeyUp() token: " + token); + } + + try { + mRemoteServiceInput.sendGamepadKeyUp(token, keyCode); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Send a gamepad axis value. + * + * Supported axes: + * <li> Left Joystick: AXIS_X, AXIS_Y + * <li> Right Joystick: AXIS_Z, AXIS_RZ + * <li> Triggers: AXIS_LTRIGGER, AXIS_RTRIGGER + * <li> DPad: AXIS_HAT_X, AXIS_HAT_Y + * + * For non-trigger axes, the range of acceptable values is [-1, 1]. The trigger axes support + * values [0, 1]. + * + * @param token identifier for the device + * @param axis MotionEvent axis + * @param value the value to send + * + * @hide + */ + public void sendGamepadAxisValue( + @NonNull IBinder token, int axis, @FloatRange(from = -1.0f, to = 1.0f) float value) + throws RuntimeException { + Objects.requireNonNull(token); + if (DEBUG_KEYS) { + Log.d(TAG, "sendGamepadAxisValue() token: " + token); + } + + try { + mRemoteServiceInput.sendGamepadAxisValue(token, axis, value); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + private final class ProviderStub extends ITvRemoteProvider.Stub { @Override public void setRemoteServiceInputSink(ITvRemoteServiceInput tvServiceInput) { diff --git a/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java b/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java index c9ce56138217..e6e39390962e 100644 --- a/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java +++ b/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java @@ -83,4 +83,52 @@ public class TvRemoteProviderTest extends AndroidTestCase { assertTrue(tvProvider.verifyTokens()); } + + @SmallTest + public void testOpenGamepadRemoteInputBridge() throws Exception { + Binder tokenA = new Binder(); + Binder tokenB = new Binder(); + Binder tokenC = new Binder(); + + class LocalTvRemoteProvider extends TvRemoteProvider { + private final ArrayList<IBinder> mTokens = new ArrayList<IBinder>(); + + LocalTvRemoteProvider(Context context) { + super(context); + } + + @Override + public void onInputBridgeConnected(IBinder token) { + mTokens.add(token); + } + + public boolean verifyTokens() { + return mTokens.size() == 3 && mTokens.contains(tokenA) && mTokens.contains(tokenB) + && mTokens.contains(tokenC); + } + } + + LocalTvRemoteProvider tvProvider = new LocalTvRemoteProvider(getContext()); + ITvRemoteProvider binder = (ITvRemoteProvider) tvProvider.getBinder(); + + ITvRemoteServiceInput tvServiceInput = mock(ITvRemoteServiceInput.class); + doAnswer((i) -> { + binder.onInputBridgeConnected(i.getArgument(0)); + return null; + }) + .when(tvServiceInput) + .openGamepadBridge(any(), any()); + + tvProvider.openGamepadBridge(tokenA, "A"); + tvProvider.openGamepadBridge(tokenB, "B"); + binder.setRemoteServiceInputSink(tvServiceInput); + tvProvider.openGamepadBridge(tokenC, "C"); + + verify(tvServiceInput).openGamepadBridge(tokenA, "A"); + verify(tvServiceInput).openGamepadBridge(tokenB, "B"); + verify(tvServiceInput).openGamepadBridge(tokenC, "C"); + verifyNoMoreInteractions(tvServiceInput); + + assertTrue(tvProvider.verifyTokens()); + } } diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java index 6ca564fb34cc..207534b36f95 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java @@ -36,6 +36,7 @@ import static com.android.mediaroutertest.StubMediaRoute2ProviderService.VOLUME_ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.content.Context; @@ -160,6 +161,7 @@ public class MediaRouter2ManagerTest { }); MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2); + assertNotNull(routeToRemove); StubMediaRoute2ProviderService sInstance = StubMediaRoute2ProviderService.getInstance(); @@ -171,6 +173,52 @@ public class MediaRouter2ManagerTest { assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); } + @Test + public void testGetRoutes_removedRoute_returnsCorrectRoutes() throws Exception { + CountDownLatch addedLatch = new CountDownLatch(1); + CountDownLatch removedLatch = new CountDownLatch(1); + + RouteCallback routeCallback = new RouteCallback() { + // Used to ensure the removed route is added. + @Override + public void onRoutesAdded(List<MediaRoute2Info> routes) { + if (removedLatch.getCount() > 0) { + return; + } + addedLatch.countDown(); + } + + @Override + public void onRoutesRemoved(List<MediaRoute2Info> routes) { + removedLatch.countDown(); + } + }; + + mRouter2.registerRouteCallback(mExecutor, routeCallback, + new RouteDiscoveryPreference.Builder(FEATURES_ALL, true).build()); + mRouteCallbacks.add(routeCallback); + + Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL); + MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2); + assertNotNull(routeToRemove); + + StubMediaRoute2ProviderService sInstance = + StubMediaRoute2ProviderService.getInstance(); + assertNotNull(sInstance); + sInstance.removeRoute(ROUTE_ID2); + + // Wait until the route is removed. + assertTrue(removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + Map<String, MediaRoute2Info> newRoutes = waitAndGetRoutesWithManager(FEATURES_ALL); + assertNull(newRoutes.get(ROUTE_ID2)); + + // Revert the removal. + sInstance.addRoute(routeToRemove); + assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + mRouter2.unregisterRouteCallback(routeCallback); + } + /** * Tests if we get proper routes for application that has special route feature. */ @@ -475,8 +523,8 @@ public class MediaRouter2ManagerTest { MediaRouter2Manager.Callback managerCallback = new MediaRouter2Manager.Callback() { @Override public void onRoutesAdded(List<MediaRoute2Info> routes) { - for (int i = 0; i < routes.size(); i++) { - if (!routes.get(i).isSystemRoute()) { + for (MediaRoute2Info route : routes) { + if (!route.isSystemRoute()) { addedLatch.countDown(); break; } diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java index 6d46ba582ddc..4e398f26366a 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java @@ -65,9 +65,9 @@ public class StubMediaRoute2ProviderService extends MediaRoute2ProviderService { public static final String ROUTE_NAME_VARIABLE_VOLUME = "Variable Volume Route"; public static final String FEATURE_SAMPLE = - "com.android.mediarouteprovider.FEATURE_SAMPLE"; + "com.android.mediaroutertest.FEATURE_SAMPLE"; public static final String FEATURE_SPECIAL = - "com.android.mediarouteprovider.FEATURE_SPECIAL"; + "com.android.mediaroutertest..FEATURE_SPECIAL"; Map<String, MediaRoute2Info> mRoutes = new HashMap<>(); Map<String, String> mRouteIdToSessionId = new HashMap<>(); diff --git a/packages/CarSystemUI/res/layout/car_qs_footer.xml b/packages/CarSystemUI/res/layout/car_qs_footer.xml deleted file mode 100644 index bf96c00e3f0d..000000000000 --- a/packages/CarSystemUI/res/layout/car_qs_footer.xml +++ /dev/null @@ -1,83 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2018 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<!-- extends RelativeLayout --> -<com.android.systemui.qs.car.CarQSFooter - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/qs_footer" - android:layout_width="match_parent" - android:layout_height="@dimen/car_qs_footer_height" - android:baselineAligned="false" - android:clickable="false" - android:clipChildren="false" - android:clipToPadding="false" - android:paddingBottom="@dimen/car_qs_footer_padding_bottom" - android:paddingTop="@dimen/car_qs_footer_padding_top" - android:paddingEnd="@dimen/car_qs_footer_padding_end" - android:paddingStart="@dimen/car_qs_footer_padding_start" - android:gravity="center_vertical"> - - <com.android.systemui.statusbar.phone.MultiUserSwitch - android:id="@+id/multi_user_switch" - android:layout_alignParentStart="true" - android:layout_centerVertical="true" - android:layout_width="@dimen/car_qs_footer_icon_width" - android:layout_height="@dimen/car_qs_footer_icon_height" - android:background="?android:attr/selectableItemBackground" - android:focusable="true"> - - <ImageView - android:id="@+id/multi_user_avatar" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - android:scaleType="fitCenter"/> - </com.android.systemui.statusbar.phone.MultiUserSwitch> - - <ImageView - android:id="@+id/user_switch_expand_icon" - android:layout_height="match_parent" - android:layout_width="@dimen/car_qs_footer_user_switch_icon_width" - android:layout_centerVertical="true" - android:layout_toEndOf="@+id/multi_user_switch" - android:layout_marginLeft="@dimen/car_qs_footer_user_switch_icon_margin" - android:layout_marginRight="@dimen/car_qs_footer_user_switch_icon_margin" - android:src="@drawable/car_ic_arrow_drop_up" - android:scaleType="fitCenter"> - </ImageView> - - <TextView android:id="@+id/user_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textSize="@dimen/car_qs_footer_user_name_text_size" - android:textColor="@color/car_qs_footer_user_name_color" - android:gravity="start|center_vertical" - android:layout_centerVertical="true" - android:layout_toEndOf="@id/user_switch_expand_icon" /> - - <com.android.systemui.statusbar.phone.SettingsButton - android:id="@+id/settings_button" - android:layout_alignParentEnd="true" - android:layout_centerVertical="true" - android:layout_width="@dimen/car_qs_footer_icon_width" - android:layout_height="@dimen/car_qs_footer_icon_height" - android:background="@drawable/ripple_drawable" - android:contentDescription="@string/accessibility_quick_settings_settings" - android:scaleType="centerCrop" - android:src="@drawable/ic_settings_16dp" - android:tint="?android:attr/colorForeground" - style="@android:style/Widget.Material.Button.Borderless" /> - -</com.android.systemui.qs.car.CarQSFooter> diff --git a/packages/CarSystemUI/res/layout/car_qs_panel.xml b/packages/CarSystemUI/res/layout/car_qs_panel.xml deleted file mode 100644 index 0c6f322ca261..000000000000 --- a/packages/CarSystemUI/res/layout/car_qs_panel.xml +++ /dev/null @@ -1,43 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2018 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/quick_settings_container" - android:clipChildren="false" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@color/car_qs_background_primary" - android:orientation="vertical" - android:elevation="4dp"> - - <include layout="@layout/car_status_bar_header"/> - <include layout="@layout/car_qs_footer"/> - - <RelativeLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/user_switcher_container" - android:clipChildren="false" - android:layout_width="match_parent" - android:layout_height="@dimen/car_user_switcher_container_height"> - - <com.android.systemui.car.userswitcher.UserGridRecyclerView - android:id="@+id/user_grid" - android:layout_width="match_parent" - android:layout_height="match_parent"/> - - </RelativeLayout> - -</LinearLayout> diff --git a/packages/CarSystemUI/res/layout/car_status_bar_header.xml b/packages/CarSystemUI/res/layout/car_status_bar_header.xml index 81c7108a4cb2..12c9f11b3064 100644 --- a/packages/CarSystemUI/res/layout/car_status_bar_header.xml +++ b/packages/CarSystemUI/res/layout/car_status_bar_header.xml @@ -15,7 +15,7 @@ ~ limitations under the License --> <!-- Extends LinearLayout --> -<com.android.systemui.qs.car.CarStatusBarHeader +<com.android.systemui.car.userswitcher.CarStatusBarHeader xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/header" android:layout_width="match_parent" @@ -27,4 +27,4 @@ android:layout_height="match_parent" android:layout_weight="1" /> -</com.android.systemui.qs.car.CarStatusBarHeader> +</com.android.systemui.car.userswitcher.CarStatusBarHeader> diff --git a/packages/CarSystemUI/res/values/colors.xml b/packages/CarSystemUI/res/values/colors.xml index 7972e09869d3..3e44721848a1 100644 --- a/packages/CarSystemUI/res/values/colors.xml +++ b/packages/CarSystemUI/res/values/colors.xml @@ -20,6 +20,7 @@ <color name="car_user_switcher_background_color">#000000</color> <color name="car_user_switcher_name_text_color">@*android:color/car_body1_light</color> <color name="car_user_switcher_add_user_background_color">#131313</color> + <color name="car_user_switcher_add_user_add_sign_color">@*android:color/car_body1_light</color> <color name="car_nav_icon_fill_color">#8Fffffff</color> <color name="car_nav_icon_fill_color_selected">#ffffff</color> <!-- colors for seekbar --> diff --git a/packages/CarSystemUI/res/values/colors_car.xml b/packages/CarSystemUI/res/values/colors_car.xml deleted file mode 100644 index 5f33f8f94a9a..000000000000 --- a/packages/CarSystemUI/res/values/colors_car.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* - * Copyright 2018, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ ---> -<resources> - <color name="car_qs_background_primary">#263238</color> <!-- Blue Gray 900 --> - <color name="car_qs_footer_user_name_color">@*android:color/car_grey_50</color> - - <!-- colors for user switcher --> - <color name="car_user_switcher_background_color">@*android:color/car_card_dark</color> - <color name="car_user_switcher_name_text_color">@*android:color/car_body1_light</color> - <color name="car_user_switcher_add_user_background_color">@*android:color/car_dark_blue_grey_600</color> - <color name="car_user_switcher_add_user_add_sign_color">@*android:color/car_body1_light</color> -</resources> diff --git a/packages/CarSystemUI/res/values/dimens.xml b/packages/CarSystemUI/res/values/dimens.xml index f68d0349e6a0..9014eb15d6cf 100644 --- a/packages/CarSystemUI/res/values/dimens.xml +++ b/packages/CarSystemUI/res/values/dimens.xml @@ -22,16 +22,11 @@ <dimen name="status_bar_icon_drawing_size_dark">36dp</dimen> <dimen name="status_bar_icon_drawing_size">36dp</dimen> - <dimen name="car_qs_header_system_icons_area_height">96dp</dimen> <!-- The amount by which to scale up the status bar icons. --> <item name="status_bar_icon_scale_factor" format="float" type="dimen">1.75</item> <dimen name="car_primary_icon_size">@*android:dimen/car_primary_icon_size</dimen> - <!-- dimensions for the car user switcher --> - <dimen name="car_user_switcher_name_text_size">@dimen/car_body1_size</dimen> - <dimen name="car_user_switcher_vertical_spacing_between_users">124dp</dimen> - <!--These values represent MIN and MAX for hvac--> <item name="hvac_min_value" format="float" type="dimen">0</item> <item name="hvac_max_value" format="float" type="dimen">126</item> @@ -90,9 +85,6 @@ <!-- The width of panel holding the notification card. --> <dimen name="notification_panel_width">522dp</dimen> - <!-- The width of the quick settings panel. -1 for match_parent. --> - <dimen name="qs_panel_width">-1px</dimen> - <!-- Height of a small notification in the status bar--> <dimen name="notification_min_height">192dp</dimen> @@ -149,7 +141,19 @@ child closer so there is less wasted space. --> <dimen name="notification_children_container_margin_top">68dp</dimen> - <!-- The height of the quick settings footer that holds the user switcher, settings icon, - etc. in the car setting.--> - <dimen name="qs_footer_height">74dp</dimen> + <!-- dimensions for the car user switcher --> + <dimen name="car_user_switcher_name_text_size">@*android:dimen/car_body1_size</dimen> + <dimen name="car_user_switcher_image_avatar_size">@*android:dimen/car_large_avatar_size</dimen> + <dimen name="car_user_switcher_vertical_spacing_between_users">@*android:dimen/car_padding_5</dimen> + <dimen name="car_user_switcher_vertical_spacing_between_name_and_avatar">@*android:dimen/car_padding_4</dimen> + <dimen name="car_user_switcher_margin_top">@*android:dimen/car_padding_4</dimen> + + <dimen name="car_navigation_button_width">64dp</dimen> + <dimen name="car_navigation_bar_width">760dp</dimen> + <dimen name="car_left_navigation_bar_width">96dp</dimen> + <dimen name="car_right_navigation_bar_width">96dp</dimen> + + <dimen name="car_user_switcher_container_height">420dp</dimen> + <!-- This must be the negative of car_user_switcher_container_height for the animation. --> + <dimen name="car_user_switcher_container_anim_height">-420dp</dimen> </resources> diff --git a/packages/CarSystemUI/res/values/dimens_car.xml b/packages/CarSystemUI/res/values/dimens_car.xml deleted file mode 100644 index e7ecf7fafc9f..000000000000 --- a/packages/CarSystemUI/res/values/dimens_car.xml +++ /dev/null @@ -1,45 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - * Copyright (c) 2018, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ ---> -<resources> - <!-- dimensions for the car user switcher --> - <dimen name="car_user_switcher_name_text_size">@*android:dimen/car_body1_size</dimen> - <dimen name="car_user_switcher_image_avatar_size">@*android:dimen/car_large_avatar_size</dimen> - <dimen name="car_user_switcher_vertical_spacing_between_users">@*android:dimen/car_padding_5</dimen> - <dimen name="car_user_switcher_vertical_spacing_between_name_and_avatar">@*android:dimen/car_padding_4</dimen> - <dimen name="car_user_switcher_margin_top">@*android:dimen/car_padding_4</dimen> - - <dimen name="car_navigation_button_width">64dp</dimen> - <dimen name="car_navigation_bar_width">760dp</dimen> - <dimen name="car_left_navigation_bar_width">96dp</dimen> - <dimen name="car_right_navigation_bar_width">96dp</dimen> - - <dimen name="car_qs_footer_height">112dp</dimen> - <dimen name="car_qs_footer_padding_bottom">16dp</dimen> - <dimen name="car_qs_footer_padding_top">16dp</dimen> - <dimen name="car_qs_footer_padding_end">46dp</dimen> - <dimen name="car_qs_footer_padding_start">46dp</dimen> - <dimen name="car_qs_footer_icon_width">56dp</dimen> - <dimen name="car_qs_footer_icon_height">56dp</dimen> - <dimen name="car_qs_footer_user_switch_icon_margin">5dp</dimen> - <dimen name="car_qs_footer_user_switch_icon_width">36dp</dimen> - <dimen name="car_qs_footer_user_name_text_size">@*android:dimen/car_body2_size</dimen> - - <dimen name="car_user_switcher_container_height">420dp</dimen> - <!-- This must be the negative of car_user_switcher_container_height for the animation. --> - <dimen name="car_user_switcher_container_anim_height">-420dp</dimen> -</resources> diff --git a/packages/CarSystemUI/res/values/ids_car.xml b/packages/CarSystemUI/res/values/ids.xml index 27ed2e250d9f..27ed2e250d9f 100644 --- a/packages/CarSystemUI/res/values/ids_car.xml +++ b/packages/CarSystemUI/res/values/ids.xml diff --git a/packages/CarSystemUI/res/values/integers.xml b/packages/CarSystemUI/res/values/integers.xml index 8b87c740425f..5ae5555a8092 100644 --- a/packages/CarSystemUI/res/values/integers.xml +++ b/packages/CarSystemUI/res/values/integers.xml @@ -16,5 +16,19 @@ --> <resources> - <integer name="user_fullscreen_switcher_num_col">2</integer> + <!-- Full screen user switcher column number --> + <integer name="user_fullscreen_switcher_num_col">3</integer> + + <!--Percentage of the screen height, from the bottom, that a notification panel being + partially closed at will result in it remaining open if released--> + <integer name="notification_settle_open_percentage">20</integer> + <!--Percentage of the screen height, from the bottom, that a notification panel being peeked + at will result in remaining closed the panel if released--> + <integer name="notification_settle_close_percentage">80</integer> + + <!-- Timeout values in milliseconds for displaying volume dialog--> + <integer name="car_volume_dialog_display_normal_timeout">3000</integer> + <integer name="car_volume_dialog_display_hovering_timeout">16000</integer> + <integer name="car_volume_dialog_display_expanded_normal_timeout">6000</integer> + <integer name="car_volume_dialog_display_expanded_hovering_timeout">32000</integer> </resources> diff --git a/packages/CarSystemUI/res/values/integers_car.xml b/packages/CarSystemUI/res/values/integers_car.xml deleted file mode 100644 index db8ce9544705..000000000000 --- a/packages/CarSystemUI/res/values/integers_car.xml +++ /dev/null @@ -1,34 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (c) 2018, The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<resources xmlns:android="http://schemas.android.com/apk/res/android"> - <!-- Full screen user switcher column number TODO: move to support library--> - <integer name="user_fullscreen_switcher_num_col">3</integer> - - <!--Percentage of the screen height, from the bottom, that a notification panel being - partially closed at will result in it remaining open if released--> - <integer name="notification_settle_open_percentage">20</integer> - <!--Percentage of the screen height, from the bottom, that a notification panel being peeked - at will result in remaining closed the panel if released--> - <integer name="notification_settle_close_percentage">80</integer> - - <!-- Timeout values in milliseconds for displaying volume dialog--> - <integer name="car_volume_dialog_display_normal_timeout">3000</integer> - <integer name="car_volume_dialog_display_hovering_timeout">16000</integer> - <integer name="car_volume_dialog_display_expanded_normal_timeout">6000</integer> - <integer name="car_volume_dialog_display_expanded_hovering_timeout">32000</integer> -</resources> diff --git a/packages/CarSystemUI/res/values/strings.xml b/packages/CarSystemUI/res/values/strings.xml index 9ea7ed027d34..881e746d633d 100644 --- a/packages/CarSystemUI/res/values/strings.xml +++ b/packages/CarSystemUI/res/values/strings.xml @@ -22,4 +22,16 @@ <string name="hvac_max_text">Max</string> <!-- Text for voice recognition toast. [CHAR LIMIT=60] --> <string name="voice_recognition_toast">Voice recognition now handled by connected Bluetooth device</string> + <!-- Name of Guest Profile. [CHAR LIMIT=30] --> + <string name="car_guest">Guest</string> + <!-- Title for button that starts a guest session. [CHAR LIMIT=30] --> + <string name="start_guest_session">Guest</string> + <!-- Title for button that adds a new user. [CHAR LIMIT=30] --> + <string name="car_add_user">Add User</string> + <!-- Default name of the new user created. [CHAR LIMIT=30] --> + <string name="car_new_user">New User</string> + <!-- Message to inform user that creation of new user requires that user to set up their space. [CHAR LIMIT=100] --> + <string name="user_add_user_message_setup">When you add a new user, that person needs to set up their space.</string> + <!-- Message to inform user that the newly created user will have permissions to update apps for all other users. [CHAR LIMIT=100] --> + <string name="user_add_user_message_update">Any user can update apps for all other users.</string> </resources> diff --git a/packages/CarSystemUI/res/values/strings_car.xml b/packages/CarSystemUI/res/values/strings_car.xml deleted file mode 100644 index 83e91c57ccc3..000000000000 --- a/packages/CarSystemUI/res/values/strings_car.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright (c) 2018, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ ---> -<resources> - <!-- Name of Guest Profile. [CHAR LIMIT=30] --> - <string name="car_guest">Guest</string> - <!-- Title for button that starts a guest session. [CHAR LIMIT=30] --> - <string name="start_guest_session">Guest</string> - <!-- Title for button that adds a new user. [CHAR LIMIT=30] --> - <string name="car_add_user">Add User</string> - <!-- Default name of the new user created. [CHAR LIMIT=30] --> - <string name="car_new_user">New User</string> - <!-- Message to inform user that creation of new user requires that user to set up their space. [CHAR LIMIT=100] --> - <string name="user_add_user_message_setup">When you add a new user, that person needs to set up their space.</string> - <!-- Message to inform user that the newly created user will have permissions to update apps for all other users. [CHAR LIMIT=100] --> - <string name="user_add_user_message_update">Any user can update apps for all other users.</string> -</resources> diff --git a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/CarStatusBarHeader.java index 4ef926fae816..bab67154e75d 100644 --- a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/CarStatusBarHeader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright (C) 2020 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.car; +package com.android.systemui.car.userswitcher; import android.content.Context; import android.graphics.Color; diff --git a/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java index 58add179886c..2ff667093e58 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java @@ -368,7 +368,7 @@ public class UserGridRecyclerView extends RecyclerView { private void applyCarSysUIDialogFlags(AlertDialog dialog) { final Window window = dialog.getWindow(); - window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); + window.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); window.getAttributes().setFitInsetsTypes( diff --git a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java deleted file mode 100644 index b74f1998bf9e..000000000000 --- a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.car; - -import android.content.Context; -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.Log; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -import com.android.systemui.Dependency; -import com.android.systemui.R; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.qs.QSFooter; -import com.android.systemui.qs.QSPanel; -import com.android.systemui.statusbar.phone.MultiUserSwitch; -import com.android.systemui.statusbar.policy.DeviceProvisionedController; -import com.android.systemui.statusbar.policy.UserInfoController; - -/** - * The footer view that displays below the status bar in the auto use-case. This view shows the - * user switcher and access to settings. - */ -public class CarQSFooter extends RelativeLayout implements QSFooter, - UserInfoController.OnUserInfoChangedListener { - private static final String TAG = "CarQSFooter"; - - private UserInfoController mUserInfoController; - - private MultiUserSwitch mMultiUserSwitch; - private TextView mUserName; - private ImageView mMultiUserAvatar; - private CarQSFragment.UserSwitchCallback mUserSwitchCallback; - - public CarQSFooter(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mMultiUserSwitch = findViewById(R.id.multi_user_switch); - mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar); - mUserName = findViewById(R.id.user_name); - - mUserInfoController = Dependency.get(UserInfoController.class); - - mMultiUserSwitch.setOnClickListener(v -> { - if (mUserSwitchCallback == null) { - Log.e(TAG, "CarQSFooter not properly set up; cannot display user switcher."); - return; - } - - if (!mUserSwitchCallback.isShowing()) { - mUserSwitchCallback.show(); - } else { - mUserSwitchCallback.hide(); - } - }); - - findViewById(R.id.settings_button).setOnClickListener(v -> { - ActivityStarter activityStarter = Dependency.get(ActivityStarter.class); - - if (!Dependency.get(DeviceProvisionedController.class).isCurrentUserSetup()) { - // If user isn't setup just unlock the device and dump them back at SUW. - activityStarter.postQSRunnableDismissingKeyguard(() -> { }); - return; - } - - activityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS), - true /* dismissShade */); - }); - } - - @Override - public void onUserInfoChanged(String name, Drawable picture, String userAccount) { - mMultiUserAvatar.setImageDrawable(picture); - mUserName.setText(name); - } - - @Override - public void setQSPanel(@Nullable QSPanel panel) { - if (panel != null) { - mMultiUserSwitch.setQsPanel(panel); - } - } - - public void setUserSwitchCallback(CarQSFragment.UserSwitchCallback callback) { - mUserSwitchCallback = callback; - } - - @Override - public void setListening(boolean listening) { - if (listening) { - mUserInfoController.addCallback(this); - } else { - mUserInfoController.removeCallback(this); - } - } - - @Override - public void setExpandClickListener(OnClickListener onClickListener) { - // No view that should expand/collapse the quick settings. - } - - @Override - public void setExpanded(boolean expanded) { - // Do nothing because the quick settings cannot be expanded. - } - - @Override - public void setExpansion(float expansion) { - // Do nothing because the quick settings cannot be expanded. - } - - @Override - public void setKeyguardShowing(boolean keyguardShowing) { - // Do nothing because the footer will not be shown when the keyguard is up. - } -} diff --git a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFragment.java b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFragment.java deleted file mode 100644 index 31965c5fc022..000000000000 --- a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFragment.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.car; - -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -import android.app.Fragment; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; - -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.GridLayoutManager; - -import com.android.systemui.R; -import com.android.systemui.car.userswitcher.UserGridRecyclerView; -import com.android.systemui.plugins.qs.QS; -import com.android.systemui.qs.QSFooter; - -import java.util.ArrayList; -import java.util.List; - -/** - * A quick settings fragment for the car. For auto, there is no row for quick settings or ability - * to expand the quick settings panel. Instead, the only thing is that displayed is the - * status bar, and a static row with access to the user switcher and settings. - */ -public class CarQSFragment extends Fragment implements QS { - private View mHeader; - private View mUserSwitcherContainer; - private CarQSFooter mFooter; - private View mFooterUserName; - private View mFooterExpandIcon; - private UserGridRecyclerView mUserGridView; - private AnimatorSet mAnimatorSet; - private UserSwitchCallback mUserSwitchCallback; - - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.car_qs_panel, container, false); - } - - @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - mHeader = view.findViewById(R.id.header); - mFooter = view.findViewById(R.id.qs_footer); - mFooterUserName = mFooter.findViewById(R.id.user_name); - mFooterExpandIcon = mFooter.findViewById(R.id.user_switch_expand_icon); - - mUserSwitcherContainer = view.findViewById(R.id.user_switcher_container); - - updateUserSwitcherHeight(0); - - Context context = getContext(); - mUserGridView = mUserSwitcherContainer.findViewById(R.id.user_grid); - GridLayoutManager layoutManager = new GridLayoutManager(context, - context.getResources().getInteger(R.integer.user_fullscreen_switcher_num_col)); - mUserGridView.setLayoutManager(layoutManager); - mUserGridView.buildAdapter(); - - mUserSwitchCallback = new UserSwitchCallback(); - mFooter.setUserSwitchCallback(mUserSwitchCallback); - } - - @Override - public void hideImmediately() { - getView().setVisibility(View.INVISIBLE); - } - - @Override - public void setQsExpansion(float qsExpansionFraction, float headerTranslation) { - // If the header is to be completed translated down, then set it to be visible. - getView().setVisibility(headerTranslation == 0 ? View.VISIBLE : View.INVISIBLE); - } - - @Override - public View getHeader() { - return mHeader; - } - - @VisibleForTesting - QSFooter getFooter() { - return mFooter; - } - - @Override - public void setHeaderListening(boolean listening) { - mFooter.setListening(listening); - } - - @Override - public void setListening(boolean listening) { - mFooter.setListening(listening); - } - - @Override - public int getQsMinExpansionHeight() { - return getView().getHeight(); - } - - @Override - public int getDesiredHeight() { - return getView().getHeight(); - } - - @Override - public void setPanelView(HeightListener notificationPanelView) { - // No quick settings panel. - } - - @Override - public void setHeightOverride(int desiredHeight) { - // No ability to expand quick settings. - } - - @Override - public void setHeaderClickable(boolean qsExpansionEnabled) { - // Usually this sets the expand button to be clickable, but there is no quick settings to - // expand. - } - - @Override - public boolean isCustomizing() { - // No ability to customize the quick settings. - return false; - } - - @Override - public void setOverscrolling(boolean overscrolling) { - // No overscrolling to reveal quick settings. - } - - @Override - public void setExpanded(boolean qsExpanded) { - // No quick settings to expand - } - - @Override - public boolean isShowingDetail() { - // No detail panel to close. - return false; - } - - @Override - public void closeDetail() { - // No detail panel to close. - } - - @Override - public void animateHeaderSlidingIn(long delay) { - // No header to animate. - } - - @Override - public void animateHeaderSlidingOut() { - // No header to animate. - } - - @Override - public void notifyCustomizeChanged() { - // There is no ability to customize quick settings. - } - - @Override - public void setContainer(ViewGroup container) { - // No quick settings, so no container to set. - } - - @Override - public void setExpandClickListener(OnClickListener onClickListener) { - // No ability to expand the quick settings. - } - - public class UserSwitchCallback { - private boolean mShowing; - - public boolean isShowing() { - return mShowing; - } - - public void show() { - mShowing = true; - animateHeightChange(true /* opening */); - } - - public void hide() { - mShowing = false; - animateHeightChange(false /* opening */); - } - } - - private void updateUserSwitcherHeight(int height) { - ViewGroup.LayoutParams layoutParams = mUserSwitcherContainer.getLayoutParams(); - layoutParams.height = height; - mUserSwitcherContainer.requestLayout(); - } - - private void animateHeightChange(boolean opening) { - // Animation in progress; cancel it to avoid contention. - if (mAnimatorSet != null) { - mAnimatorSet.cancel(); - } - - List<Animator> allAnimators = new ArrayList<>(); - ValueAnimator heightAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(getContext(), - opening ? R.anim.car_user_switcher_open_animation - : R.anim.car_user_switcher_close_animation); - heightAnimator.addUpdateListener(valueAnimator -> { - updateUserSwitcherHeight((Integer) valueAnimator.getAnimatedValue()); - }); - allAnimators.add(heightAnimator); - - Animator nameAnimator = AnimatorInflater.loadAnimator(getContext(), - opening ? R.anim.car_user_switcher_open_name_animation - : R.anim.car_user_switcher_close_name_animation); - nameAnimator.setTarget(mFooterUserName); - allAnimators.add(nameAnimator); - - Animator iconAnimator = AnimatorInflater.loadAnimator(getContext(), - opening ? R.anim.car_user_switcher_open_icon_animation - : R.anim.car_user_switcher_close_icon_animation); - iconAnimator.setTarget(mFooterExpandIcon); - allAnimators.add(iconAnimator); - - mAnimatorSet = new AnimatorSet(); - mAnimatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mAnimatorSet = null; - } - }); - mAnimatorSet.playTogether(allAnimators.toArray(new Animator[0])); - - // Setup all values to the start values in the animations, since there are delays, but need - // to have all values start at the beginning. - setupInitialValues(mAnimatorSet); - - mAnimatorSet.start(); - } - - private void setupInitialValues(Animator anim) { - if (anim instanceof AnimatorSet) { - for (Animator a : ((AnimatorSet) anim).getChildAnimations()) { - setupInitialValues(a); - } - } else if (anim instanceof ObjectAnimator) { - ((ObjectAnimator) anim).setCurrentFraction(0.0f); - } - } -} diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java index d8111d04348b..ec1dabc1bd72 100644 --- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java +++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java @@ -54,7 +54,6 @@ import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.PluginDependencyProvider; import com.android.systemui.plugins.qs.QS; -import com.android.systemui.qs.car.CarQSFragment; import com.android.systemui.recents.Recents; import com.android.systemui.recents.ScreenPinningRequest; import com.android.systemui.shared.plugins.PluginManager; @@ -407,7 +406,7 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt @Override protected QS createDefaultQSFragment() { - return new CarQSFragment(); + return null; } private BatteryController createBatteryController() { diff --git a/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java b/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java index 0dbe1a3ea1dd..04d69eabaeac 100644 --- a/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java +++ b/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java @@ -90,7 +90,7 @@ public class SystemUIOverlayWindowController implements mLp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL, + WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp index 587109d5dae9..54986a43a870 100644 --- a/packages/CtsShim/build/Android.bp +++ b/packages/CtsShim/build/Android.bp @@ -69,6 +69,14 @@ android_app { // Explicitly uncompress native libs rather than letting the build system doing it and destroy the // v2/v3 signature. use_embedded_native_libs: true, + apex_available: [ + "com.android.apex.cts.shim.v1", + "com.android.apex.cts.shim.v2", + "com.android.apex.cts.shim.v2_no_hashtree", + "com.android.apex.cts.shim.v2_legacy", + "com.android.apex.cts.shim.v2_sdk_target_p", + "com.android.apex.cts.shim.v3", + ], } //########################################################## @@ -110,7 +118,10 @@ android_app { dex_preopt: { enabled: false, }, - manifest: "shim/AndroidManifestTargetPSdk.xml" + manifest: "shim/AndroidManifestTargetPSdk.xml", + apex_available: [ + "com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p", + ], } //########################################################## @@ -128,4 +139,12 @@ android_app { }, manifest: "shim/AndroidManifest.xml", + apex_available: [ + "com.android.apex.cts.shim.v1", + "com.android.apex.cts.shim.v2", + "com.android.apex.cts.shim.v2_no_hashtree", + "com.android.apex.cts.shim.v2_legacy", + "com.android.apex.cts.shim.v2_sdk_target_p", + "com.android.apex.cts.shim.v3", + ], } diff --git a/packages/CtsShim/build/jni/Android.bp b/packages/CtsShim/build/jni/Android.bp index ea15b43416b4..7a5b07e61e9d 100644 --- a/packages/CtsShim/build/jni/Android.bp +++ b/packages/CtsShim/build/jni/Android.bp @@ -18,4 +18,13 @@ cc_library_shared { name: "libshim_jni", srcs: ["Shim.c"], sdk_version: "24", + apex_available: [ + "//apex_available:platform", + "com.android.apex.cts.shim.v1", + "com.android.apex.cts.shim.v2", + "com.android.apex.cts.shim.v2_no_hashtree", + "com.android.apex.cts.shim.v2_legacy", + "com.android.apex.cts.shim.v2_sdk_target_p", + "com.android.apex.cts.shim.v3", + ], } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java index e551b69e024a..ee8fb38ef08c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java @@ -38,7 +38,7 @@ public class BluetoothMediaDevice extends MediaDevice { BluetoothMediaDevice(Context context, CachedBluetoothDevice device, MediaRouter2Manager routerManager, MediaRoute2Info info, String packageName) { - super(context, MediaDeviceType.TYPE_BLUETOOTH_DEVICE, routerManager, info, packageName); + super(context, routerManager, info, packageName); mCachedDevice = device; initDeviceRecord(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java index 85fa988a866e..83a96716e284 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java @@ -38,7 +38,7 @@ public class InfoMediaDevice extends MediaDevice { InfoMediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info, String packageName) { - super(context, MediaDeviceType.TYPE_CAST_DEVICE, routerManager, info, packageName); + super(context, routerManager, info, packageName); initDeviceRecord(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java index 39e6a129a992..6aff301f57d4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -15,6 +15,16 @@ */ package com.android.settingslib.media; +import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; +import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_GROUP; +import static android.media.MediaRoute2Info.TYPE_HEARING_AID; +import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; +import static android.media.MediaRoute2Info.TYPE_UNKNOWN; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; + import android.content.Context; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; @@ -38,13 +48,21 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { private static final String TAG = "MediaDevice"; @Retention(RetentionPolicy.SOURCE) - @IntDef({MediaDeviceType.TYPE_CAST_DEVICE, + @IntDef({MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE, + MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE, + MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE, MediaDeviceType.TYPE_BLUETOOTH_DEVICE, + MediaDeviceType.TYPE_CAST_DEVICE, + MediaDeviceType.TYPE_CAST_GROUP_DEVICE, MediaDeviceType.TYPE_PHONE_DEVICE}) public @interface MediaDeviceType { - int TYPE_PHONE_DEVICE = 1; - int TYPE_CAST_DEVICE = 2; - int TYPE_BLUETOOTH_DEVICE = 3; + int TYPE_USB_C_AUDIO_DEVICE = 1; + int TYPE_3POINT5_MM_AUDIO_DEVICE = 2; + int TYPE_FAST_PAIR_BLUETOOTH_DEVICE = 3; + int TYPE_BLUETOOTH_DEVICE = 4; + int TYPE_CAST_DEVICE = 5; + int TYPE_CAST_GROUP_DEVICE = 6; + int TYPE_PHONE_DEVICE = 7; } @VisibleForTesting @@ -58,13 +76,43 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { protected final MediaRouter2Manager mRouterManager; protected final String mPackageName; - MediaDevice(Context context, @MediaDeviceType int type, MediaRouter2Manager routerManager, - MediaRoute2Info info, String packageName) { - mType = type; + MediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info, + String packageName) { mContext = context; mRouteInfo = info; mRouterManager = routerManager; mPackageName = packageName; + setType(info); + } + + private void setType(MediaRoute2Info info) { + if (info == null) { + mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE; + return; + } + + switch (info.getType()) { + case TYPE_GROUP: + mType = MediaDeviceType.TYPE_CAST_GROUP_DEVICE; + break; + case TYPE_BUILTIN_SPEAKER: + mType = MediaDeviceType.TYPE_PHONE_DEVICE; + break; + case TYPE_WIRED_HEADSET: + case TYPE_WIRED_HEADPHONES: + mType = MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE; + break; + case TYPE_HEARING_AID: + case TYPE_BLUETOOTH_A2DP: + mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE; + break; + case TYPE_UNKNOWN: + case TYPE_REMOTE_TV: + case TYPE_REMOTE_SPEAKER: + default: + mType = MediaDeviceType.TYPE_CAST_DEVICE; + break; + } } void initDeviceRecord() { diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index af88723c6249..c6c5ade90eb5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -42,7 +42,7 @@ public class PhoneMediaDevice extends MediaDevice { PhoneMediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info, String packageName) { - super(context, MediaDeviceType.TYPE_PHONE_DEVICE, routerManager, info, packageName); + super(context, routerManager, info, packageName); initDeviceRecord(); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java index 4b08387275be..db05b768f5db 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java @@ -15,6 +15,10 @@ */ package com.android.settingslib.media; +import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; +import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.verify; @@ -144,12 +148,19 @@ public class MediaDeviceTest { when(mCachedDevice2.isConnected()).thenReturn(true); when(mCachedDevice3.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); when(mCachedDevice3.isConnected()).thenReturn(true); + when(mBluetoothRouteInfo1.getType()).thenReturn(TYPE_BLUETOOTH_A2DP); + when(mBluetoothRouteInfo2.getType()).thenReturn(TYPE_BLUETOOTH_A2DP); + when(mBluetoothRouteInfo3.getType()).thenReturn(TYPE_BLUETOOTH_A2DP); when(mRouteInfo1.getId()).thenReturn(ROUTER_ID_1); when(mRouteInfo2.getId()).thenReturn(ROUTER_ID_2); when(mRouteInfo3.getId()).thenReturn(ROUTER_ID_3); when(mRouteInfo1.getName()).thenReturn(DEVICE_NAME_1); when(mRouteInfo2.getName()).thenReturn(DEVICE_NAME_2); when(mRouteInfo3.getName()).thenReturn(DEVICE_NAME_3); + when(mRouteInfo1.getType()).thenReturn(TYPE_REMOTE_SPEAKER); + when(mRouteInfo2.getType()).thenReturn(TYPE_REMOTE_SPEAKER); + when(mRouteInfo3.getType()).thenReturn(TYPE_REMOTE_SPEAKER); + when(mPhoneRouteInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager); when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile); when(mProfileManager.getHearingAidProfile()).thenReturn(mHapProfile); @@ -271,12 +282,12 @@ public class MediaDeviceTest { @Test public void compareTo_info_bluetooth_infoFirst() { - mMediaDevices.add(mBluetoothMediaDevice1); mMediaDevices.add(mInfoMediaDevice1); + mMediaDevices.add(mBluetoothMediaDevice1); - assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); - Collections.sort(mMediaDevices, COMPARATOR); assertThat(mMediaDevices.get(0)).isEqualTo(mInfoMediaDevice1); + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); } @Test @@ -327,7 +338,7 @@ public class MediaDeviceTest { // 5.mBluetoothMediaDevice2: * 2 times usage // 6.mBluetoothMediaDevice3: * 1 time usage // 7.mPhoneMediaDevice: * 0 time usage - // Order: 7 -> 2 -> 1 -> 3 -> 5 -> 4 -> 6 + // Order: 7 -> 2 -> 1 -> 5 -> 3 -> 6 -> 4 @Test public void compareTo_mixedDevices_carKitFirst() { when(mDevice1.getBluetoothClass()).thenReturn(mCarkitClass); @@ -352,10 +363,10 @@ public class MediaDeviceTest { assertThat(mMediaDevices.get(0)).isEqualTo(mPhoneMediaDevice); assertThat(mMediaDevices.get(1)).isEqualTo(mBluetoothMediaDevice1); assertThat(mMediaDevices.get(2)).isEqualTo(mInfoMediaDevice1); - assertThat(mMediaDevices.get(3)).isEqualTo(mInfoMediaDevice2); - assertThat(mMediaDevices.get(4)).isEqualTo(mBluetoothMediaDevice2); - assertThat(mMediaDevices.get(5)).isEqualTo(mInfoMediaDevice3); - assertThat(mMediaDevices.get(6)).isEqualTo(mBluetoothMediaDevice3); + assertThat(mMediaDevices.get(3)).isEqualTo(mBluetoothMediaDevice2); + assertThat(mMediaDevices.get(4)).isEqualTo(mInfoMediaDevice2); + assertThat(mMediaDevices.get(5)).isEqualTo(mBluetoothMediaDevice3); + assertThat(mMediaDevices.get(6)).isEqualTo(mInfoMediaDevice3); } // 1.mInfoMediaDevice1: Last Selected device @@ -365,7 +376,7 @@ public class MediaDeviceTest { // 5.mBluetoothMediaDevice2: * 4 times usage not connected // 6.mBluetoothMediaDevice3: * 1 time usage // 7.mPhoneMediaDevice: * 0 time usage - // Order: 7 -> 1 -> 3 -> 4 -> 6 -> 2 -> 5 + // Order: 7 -> 1 -> 3 -> 6 -> 4 -> 2 -> 5 @Test public void compareTo_mixedDevices_connectDeviceFirst() { when(mDevice1.getBluetoothClass()).thenReturn(mCarkitClass); @@ -394,8 +405,8 @@ public class MediaDeviceTest { assertThat(mMediaDevices.get(0)).isEqualTo(mPhoneMediaDevice); assertThat(mMediaDevices.get(1)).isEqualTo(mInfoMediaDevice1); assertThat(mMediaDevices.get(2)).isEqualTo(mInfoMediaDevice2); - assertThat(mMediaDevices.get(3)).isEqualTo(mInfoMediaDevice3); - assertThat(mMediaDevices.get(4)).isEqualTo(mBluetoothMediaDevice3); + assertThat(mMediaDevices.get(3)).isEqualTo(mBluetoothMediaDevice3); + assertThat(mMediaDevices.get(4)).isEqualTo(mInfoMediaDevice3); assertThat(mMediaDevices.get(5)).isEqualTo(mBluetoothMediaDevice1); assertThat(mMediaDevices.get(6)).isEqualTo(mBluetoothMediaDevice2); } diff --git a/packages/SystemUI/res/layout/global_actions_grid_v2.xml b/packages/SystemUI/res/layout/global_actions_grid_v2.xml index dba003aa82a9..1c4ec64e9620 100644 --- a/packages/SystemUI/res/layout/global_actions_grid_v2.xml +++ b/packages/SystemUI/res/layout/global_actions_grid_v2.xml @@ -15,8 +15,7 @@ android:clipChildren="false" android:clipToPadding="false" android:layout_marginTop="@dimen/global_actions_top_margin" - android:layout_marginLeft="@dimen/global_actions_side_margin" - android:layout_marginRight="@dimen/global_actions_side_margin" + android:layout_marginStart="@dimen/global_actions_side_margin" > <LinearLayout android:id="@android:id/list" diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java index aa2fe3c7f8fc..57b3761c294f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java @@ -16,7 +16,6 @@ package com.android.keyguard; -import android.app.Activity; import android.app.ActivityManager; import android.content.Context; import android.content.res.ColorStateList; @@ -31,6 +30,8 @@ import android.util.Log; import android.view.KeyEvent; import android.widget.FrameLayout; +import androidx.annotation.VisibleForTesting; + import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityContainer.SecurityCallback; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; @@ -101,7 +102,8 @@ public class KeyguardHostView extends FrameLayout implements SecurityCallback { public static final boolean DEBUG = KeyguardConstants.DEBUG; private static final String TAG = "KeyguardViewBase"; - private KeyguardSecurityContainer mSecurityContainer; + @VisibleForTesting + protected KeyguardSecurityContainer mSecurityContainer; public KeyguardHostView(Context context) { this(context, null); @@ -446,4 +448,11 @@ public class KeyguardHostView extends FrameLayout implements SecurityCallback { public SecurityMode getCurrentSecurityMode() { return mSecurityContainer.getCurrentSecurityMode(); } + + /** + * When bouncer was visible and is starting to become hidden. + */ + public void onStartingToHide() { + mSecurityContainer.onStartingToHide(); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java index 718bcf16c832..65bf7e6e5025 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java @@ -152,6 +152,11 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView mImm.hideSoftInputFromWindow(getWindowToken(), 0); } + @Override + public void onStartingToHide() { + mImm.hideSoftInputFromWindow(getWindowToken(), 0); + } + private void updateSwitchImeButton() { // If there's more than one IME, enable the IME switcher button final boolean wasVisible = mSwitchImeButton.getVisibility() == View.VISIBLE; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 502c0787fe38..9cfcc52134ce 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -227,6 +227,13 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe } @Override + public void onStartingToHide() { + if (mCurrentSecuritySelection != SecurityMode.None) { + getSecurityView(mCurrentSecuritySelection).onStartingToHide(); + } + } + + @Override public boolean shouldDelayChildPressedState() { return true; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java index 20b1e0d2c822..43cef3acf147 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java @@ -159,4 +159,9 @@ public interface KeyguardSecurityView { default boolean disallowInterceptTouch(MotionEvent event) { return false; } + + /** + * When bouncer was visible but is being dragged down or dismissed. + */ + default void onStartingToHide() {}; } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java index a7c40435b3ca..8f3dc224384b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java @@ -19,8 +19,10 @@ package com.android.systemui.dagger; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; +import android.app.ActivityTaskManager; import android.app.AlarmManager; import android.app.IActivityManager; +import android.app.IActivityTaskManager; import android.app.IWallpaperManager; import android.app.KeyguardManager; import android.app.NotificationManager; @@ -128,6 +130,12 @@ public class SystemServicesModule { return ActivityManager.getService(); } + @Singleton + @Provides + static IActivityTaskManager provideIActivityTaskManager() { + return ActivityTaskManager.getService(); + } + @Provides @Singleton static IBatteryStats provideIBatteryStats() { diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java index 2c1bd2186dea..322660521ee0 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java @@ -78,6 +78,7 @@ import android.widget.BaseAdapter; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; import android.widget.ListPopupWindow; import android.widget.ListView; import android.widget.TextView; @@ -2053,8 +2054,17 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, if (overflowButton != null) { if (mOverflowAdapter.getCount() > 0) { overflowButton.setOnClickListener((view) -> showPowerOverflowMenu()); + LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams) mGlobalActionsLayout.getLayoutParams(); + params.setMarginEnd(0); + mGlobalActionsLayout.setLayoutParams(params); } else { overflowButton.setVisibility(View.GONE); + LinearLayout.LayoutParams params = + (LinearLayout.LayoutParams) mGlobalActionsLayout.getLayoutParams(); + params.setMarginEnd(mContext.getResources().getDimensionPixelSize( + com.android.systemui.R.dimen.global_actions_side_margin)); + mGlobalActionsLayout.setLayoutParams(params); } } @@ -2220,6 +2230,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, }) .start(); dismissPanel(); + dismissOverflow(); resetOrientation(); } @@ -2227,6 +2238,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mShowing = false; if (mControlsUiController != null) mControlsUiController.hide(); dismissPanel(); + dismissOverflow(); resetOrientation(); completeDismiss(); } @@ -2243,6 +2255,12 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } } + private void dismissOverflow() { + if (mOverflowPopup != null) { + mOverflowPopup.dismiss(); + } + } + private void setRotationSuggestionsEnabled(boolean enabled) { try { final int userId = Binder.getCallingUserHandle().getIdentifier(); diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 9217eb161a87..f25de6a553e8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -21,20 +21,21 @@ import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.Icon; import android.graphics.drawable.RippleDrawable; +import android.media.MediaDescription; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.PlaybackState; +import android.service.media.MediaBrowserService; import android.util.Log; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnAttachStateChangeListener; @@ -55,6 +56,7 @@ import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.qs.QSMediaBrowser; import com.android.systemui.util.Assert; import java.util.List; @@ -67,7 +69,7 @@ public class MediaControlPanel { private static final String TAG = "MediaControlPanel"; @Nullable private final LocalMediaManager mLocalMediaManager; private final Executor mForegroundExecutor; - private final Executor mBackgroundExecutor; + protected final Executor mBackgroundExecutor; private Context mContext; protected LinearLayout mMediaNotifView; @@ -76,13 +78,18 @@ public class MediaControlPanel { private MediaController mController; private int mForegroundColor; private int mBackgroundColor; - protected ComponentName mRecvComponent; private MediaDevice mDevice; + protected ComponentName mServiceComponent; private boolean mIsRegistered = false; private String mKey; private final int[] mActionIds; + public static final String MEDIA_PREFERENCES = "media_control_prefs"; + public static final String MEDIA_PREFERENCE_KEY = "browser_components"; + private SharedPreferences mSharedPrefs; + private boolean mCheckedForResumption = false; + // Button IDs used in notifications protected static final int[] NOTIF_ACTION_IDS = { com.android.internal.R.id.action0, @@ -154,7 +161,6 @@ public class MediaControlPanel { * Initialize a new control panel * @param context * @param parent - * @param manager * @param routeManager Manager used to listen for device change events. * @param layoutId layout resource to use for this control panel * @param actionIds resource IDs for action buttons in the layout @@ -198,47 +204,50 @@ public class MediaControlPanel { /** * Update the media panel view for the given media session * @param token - * @param icon + * @param iconDrawable * @param iconColor * @param bgColor * @param contentIntent * @param appNameString * @param key */ - public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, + public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, int iconColor, int bgColor, PendingIntent contentIntent, String appNameString, String key) { - mToken = token; + // Ensure that component names are updated if token has changed + if (mToken == null || !mToken.equals(token)) { + mToken = token; + mServiceComponent = null; + mCheckedForResumption = false; + } + mForegroundColor = iconColor; mBackgroundColor = bgColor; mController = new MediaController(mContext, mToken); mKey = key; - MediaMetadata mediaMetadata = mController.getMetadata(); - - // Try to find a receiver for the media button that matches this app - PackageManager pm = mContext.getPackageManager(); - Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON); - List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser()); - if (info != null) { - for (ResolveInfo inf : info) { - if (inf.activityInfo.packageName.equals(mController.getPackageName())) { - mRecvComponent = inf.getComponentInfo().getComponentName(); + // Try to find a browser service component for this app + // TODO also check for a media button receiver intended for restarting (b/154127084) + // Only check if we haven't tried yet or the session token changed + String pkgName = mController.getPackageName(); + if (mServiceComponent == null && !mCheckedForResumption) { + Log.d(TAG, "Checking for service component"); + PackageManager pm = mContext.getPackageManager(); + Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE); + List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0); + if (resumeInfo != null) { + for (ResolveInfo inf : resumeInfo) { + if (inf.serviceInfo.packageName.equals(mController.getPackageName())) { + mBackgroundExecutor.execute(() -> + tryUpdateResumptionList(inf.getComponentInfo().getComponentName())); + break; + } } } + mCheckedForResumption = true; } mController.registerCallback(mSessionCallback); - if (mediaMetadata == null) { - Log.e(TAG, "Media metadata was null"); - return; - } - - ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); - if (albumView != null) { - // Resize art in a background thread - mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView)); - } mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor)); // Click action @@ -256,32 +265,9 @@ public class MediaControlPanel { // App icon ImageView appIcon = mMediaNotifView.findViewById(R.id.icon); - Drawable iconDrawable = icon.loadDrawable(mContext); iconDrawable.setTint(mForegroundColor); appIcon.setImageDrawable(iconDrawable); - // Song name - TextView titleText = mMediaNotifView.findViewById(R.id.header_title); - String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); - titleText.setText(songName); - titleText.setTextColor(mForegroundColor); - - // Not in mini player: - // App title - TextView appName = mMediaNotifView.findViewById(R.id.app_name); - if (appName != null) { - appName.setText(appNameString); - appName.setTextColor(mForegroundColor); - } - - // Artist name - TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); - if (artistText != null) { - String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); - artistText.setText(artistName); - artistText.setTextColor(mForegroundColor); - } - // Transfer chip mSeamless = mMediaNotifView.findViewById(R.id.media_seamless); if (mSeamless != null && mLocalMediaManager != null) { @@ -300,6 +286,39 @@ public class MediaControlPanel { } makeActive(); + + // App title (not in mini player) + TextView appName = mMediaNotifView.findViewById(R.id.app_name); + if (appName != null) { + appName.setText(appNameString); + appName.setTextColor(mForegroundColor); + } + + MediaMetadata mediaMetadata = mController.getMetadata(); + if (mediaMetadata == null) { + Log.e(TAG, "Media metadata was null"); + return; + } + + ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); + if (albumView != null) { + // Resize art in a background thread + mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView)); + } + + // Song name + TextView titleText = mMediaNotifView.findViewById(R.id.header_title); + String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); + titleText.setText(songName); + titleText.setTextColor(mForegroundColor); + + // Artist name (not in mini player) + TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); + if (artistText != null) { + String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); + artistText.setText(artistName); + artistText.setTextColor(mForegroundColor); + } } /** @@ -320,9 +339,12 @@ public class MediaControlPanel { /** * Get the name of the package associated with the current media controller - * @return the package name + * @return the package name, or null if no controller */ public String getMediaPlayerPackage() { + if (mController == null) { + return null; + } return mController.getPackageName(); } @@ -370,11 +392,27 @@ public class MediaControlPanel { /** * Process album art for layout + * @param description media description + * @param albumView view to hold the album art + */ + protected void processAlbumArt(MediaDescription description, ImageView albumView) { + Bitmap albumArt = description.getIconBitmap(); + //TODO check other fields (b/151054111, b/152067055) + processAlbumArtInternal(albumArt, albumView); + } + + /** + * Process album art for layout * @param metadata media metadata * @param albumView view to hold the album art */ private void processAlbumArt(MediaMetadata metadata, ImageView albumView) { Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); + //TODO check other fields (b/151054111, b/152067055) + processAlbumArtInternal(albumArt, albumView); + } + + private void processAlbumArtInternal(Bitmap albumArt, ImageView albumView) { float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius); RoundedBitmapDrawable roundedDrawable = null; if (albumArt != null) { @@ -449,10 +487,24 @@ public class MediaControlPanel { } /** - * Put controls into a resumption state + * Puts controls into a resumption state if possible, or calls removePlayer if no component was + * found that could resume playback */ public void clearControls() { Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage()); + if (mServiceComponent == null) { + // If we don't have a way to resume, just remove the player altogether + Log.d(TAG, "Removing unresumable controls"); + removePlayer(); + return; + } + resetButtons(); + } + + /** + * Hide the media buttons and show only a restart button + */ + protected void resetButtons() { // Hide all the old buttons for (int i = 0; i < mActionIds.length; i++) { ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]); @@ -465,27 +517,8 @@ public class MediaControlPanel { ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]); btn.setOnClickListener(v -> { Log.d(TAG, "Attempting to restart session"); - // Send a media button event to previously found receiver - if (mRecvComponent != null) { - Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); - intent.setComponent(mRecvComponent); - int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY; - intent.putExtra( - Intent.EXTRA_KEY_EVENT, - new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); - mContext.sendBroadcast(intent); - } else { - // If we don't have a receiver, try relaunching the activity instead - if (mController.getSessionActivity() != null) { - try { - mController.getSessionActivity().send(); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Pending intent was canceled", e); - } - } else { - Log.e(TAG, "No receiver or activity to restart"); - } - } + QSMediaBrowser browser = new QSMediaBrowser(mContext, null, mServiceComponent); + browser.restart(); }); btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play)); btn.setImageTintList(ColorStateList.valueOf(mForegroundColor)); @@ -514,4 +547,65 @@ public class MediaControlPanel { } } + /** + * Verify that we can connect to the given component with a MediaBrowser, and if so, add that + * component to the list of resumption components + */ + private void tryUpdateResumptionList(ComponentName componentName) { + Log.d(TAG, "Testing if we can connect to " + componentName); + QSMediaBrowser.testConnection(mContext, + new QSMediaBrowser.Callback() { + @Override + public void onConnected() { + Log.d(TAG, "yes we can resume with " + componentName); + mServiceComponent = componentName; + updateResumptionList(componentName); + } + + @Override + public void onError() { + Log.d(TAG, "Cannot resume with " + componentName); + mServiceComponent = null; + clearControls(); + // remove + } + }, + componentName); + } + + /** + * Add the component to the saved list of media browser services, checking for duplicates and + * removing older components that exceed the maximum limit + * @param componentName + */ + private synchronized void updateResumptionList(ComponentName componentName) { + // Add to front of saved list + if (mSharedPrefs == null) { + mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0); + } + String componentString = componentName.flattenToString(); + String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null); + if (listString == null) { + listString = componentString; + } else { + String[] components = listString.split(QSMediaBrowser.DELIMITER); + StringBuilder updated = new StringBuilder(componentString); + int nBrowsers = 1; + for (int i = 0; i < components.length + && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) { + if (componentString.equals(components[i])) { + continue; + } + updated.append(QSMediaBrowser.DELIMITER).append(components[i]); + nBrowsers++; + } + listString = updated.toString(); + } + mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply(); + } + + /** + * Called when a player can't be resumed to give it an opportunity to hide or remove itself + */ + protected void removePlayer() { } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java new file mode 100644 index 000000000000..302b84203641 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.MediaDescription; +import android.media.browse.MediaBrowser; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.os.Bundle; +import android.service.media.MediaBrowserService; +import android.util.Log; + +import java.util.List; + +/** + * Media browser for managing resumption in QS media controls + */ +public class QSMediaBrowser { + + /** Maximum number of controls to show on boot */ + public static final int MAX_RESUMPTION_CONTROLS = 5; + + /** Delimiter for saved component names */ + public static final String DELIMITER = ":"; + + private static final String TAG = "QSMediaBrowser"; + private final Context mContext; + private final Callback mCallback; + private MediaBrowser mMediaBrowser; + private ComponentName mComponentName; + + /** + * Initialize a new media browser + * @param context the context + * @param callback used to report media items found + * @param componentName Component name of the MediaBrowserService this browser will connect to + */ + public QSMediaBrowser(Context context, Callback callback, ComponentName componentName) { + mContext = context; + mCallback = callback; + mComponentName = componentName; + + Bundle rootHints = new Bundle(); + rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); + mMediaBrowser = new MediaBrowser(mContext, + mComponentName, + mConnectionCallback, + rootHints); + } + + /** + * Connects to the MediaBrowserService and looks for valid media. If a media item is returned + * by the service, QSMediaBrowser.Callback#addTrack will be called with its MediaDescription + */ + public void findRecentMedia() { + Log.d(TAG, "Connecting to " + mComponentName); + mMediaBrowser.connect(); + } + + private final MediaBrowser.SubscriptionCallback mSubscriptionCallback = + new MediaBrowser.SubscriptionCallback() { + @Override + public void onChildrenLoaded(String parentId, + List<MediaBrowser.MediaItem> children) { + if (children.size() == 0) { + Log.e(TAG, "No children found"); + return; + } + // We ask apps to return a playable item as the first child when sending + // a request with EXTRA_RECENT; if they don't, no resume controls + MediaBrowser.MediaItem child = children.get(0); + MediaDescription desc = child.getDescription(); + if (child.isPlayable()) { + mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), QSMediaBrowser.this); + } else { + Log.e(TAG, "Child found but not playable for " + mComponentName); + } + mMediaBrowser.disconnect(); + } + + @Override + public void onError(String parentId) { + Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId); + mMediaBrowser.disconnect(); + } + + @Override + public void onError(String parentId, Bundle options) { + Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId + + ", options: " + options); + mMediaBrowser.disconnect(); + } + }; + + private final MediaBrowser.ConnectionCallback mConnectionCallback = + new MediaBrowser.ConnectionCallback() { + /** + * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed. + * For resumption controls, apps are expected to return a playable media item as the first + * child. If there are no children or it isn't playable it will be ignored. + */ + @Override + public void onConnected() { + if (mMediaBrowser.isConnected()) { + mCallback.onConnected(); + Log.d(TAG, "Service connected for " + mComponentName); + String root = mMediaBrowser.getRoot(); + mMediaBrowser.subscribe(root, mSubscriptionCallback); + } + } + + /** + * Invoked when the client is disconnected from the media browser. + */ + @Override + public void onConnectionSuspended() { + Log.d(TAG, "Connection suspended for " + mComponentName); + } + + /** + * Invoked when the connection to the media browser failed. + */ + @Override + public void onConnectionFailed() { + Log.e(TAG, "Connection failed for " + mComponentName); + mCallback.onError(); + } + }; + + /** + * Connects to the MediaBrowserService and starts playback + */ + public void restart() { + if (mMediaBrowser.isConnected()) { + mMediaBrowser.disconnect(); + } + Bundle rootHints = new Bundle(); + rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); + mMediaBrowser = new MediaBrowser(mContext, mComponentName, + new MediaBrowser.ConnectionCallback() { + @Override + public void onConnected() { + Log.d(TAG, "Connected for restart " + mMediaBrowser.isConnected()); + MediaSession.Token token = mMediaBrowser.getSessionToken(); + MediaController controller = new MediaController(mContext, token); + controller.getTransportControls(); + controller.getTransportControls().prepare(); + controller.getTransportControls().play(); + } + }, rootHints); + mMediaBrowser.connect(); + } + + /** + * Get the media session token + * @return the token, or null if the MediaBrowser is null or disconnected + */ + public MediaSession.Token getToken() { + if (mMediaBrowser == null || !mMediaBrowser.isConnected()) { + return null; + } + return mMediaBrowser.getSessionToken(); + } + + /** + * Get an intent to launch the app associated with this browser service + * @return + */ + public PendingIntent getAppIntent() { + PackageManager pm = mContext.getPackageManager(); + Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName()); + return PendingIntent.getActivity(mContext, 0, launchIntent, 0); + } + + /** + * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser + * @param mContext the context + * @param callback methods onConnected or onError will be called to indicate whether the + * connection was successful or not + * @param mComponentName Component name of the MediaBrowserService this browser will connect to + */ + public static MediaBrowser testConnection(Context mContext, Callback callback, + ComponentName mComponentName) { + final MediaBrowser.ConnectionCallback mConnectionCallback = + new MediaBrowser.ConnectionCallback() { + @Override + public void onConnected() { + Log.d(TAG, "connected"); + callback.onConnected(); + } + + @Override + public void onConnectionSuspended() { + Log.d(TAG, "suspended"); + callback.onError(); + } + + @Override + public void onConnectionFailed() { + Log.d(TAG, "failed"); + callback.onError(); + } + }; + Bundle rootHints = new Bundle(); + rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); + MediaBrowser browser = new MediaBrowser(mContext, + mComponentName, + mConnectionCallback, + rootHints); + browser.connect(); + return browser; + } + + /** + * Interface to handle results from QSMediaBrowser + */ + public static class Callback { + /** + * Called when the browser has successfully connected to the service + */ + public void onConnected() { + } + + /** + * Called when the browser encountered an error connecting to the service + */ + public void onError() { + } + + /** + * Called when the browser finds a suitable track to add to the media carousel + * @param track media info for the item + * @param component component of the MediaBrowserService which returned this + * @param browser reference to the browser + */ + public void addTrack(MediaDescription track, ComponentName component, + QSMediaBrowser browser) { + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java index 89b22bc518bb..0f065661a470 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java @@ -18,11 +18,12 @@ package com.android.systemui.qs; import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle; -import android.app.Notification; +import android.app.PendingIntent; import android.content.Context; +import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; +import android.media.MediaDescription; import android.media.session.MediaController; import android.media.session.MediaSession; import android.util.Log; @@ -60,9 +61,11 @@ public class QSMediaPlayer extends MediaControlPanel { }; private final QSPanel mParent; + private final Executor mForegroundExecutor; private final DelayableExecutor mBackgroundExecutor; private final SeekBarViewModel mSeekBarViewModel; private final SeekBarObserver mSeekBarObserver; + private String mPackageName; /** * Initialize quick shade version of player @@ -77,6 +80,7 @@ public class QSMediaPlayer extends MediaControlPanel { super(context, parent, routeManager, R.layout.qs_media_panel, QS_ACTION_IDS, foregroundExecutor, backgroundExecutor); mParent = (QSPanel) parent; + mForegroundExecutor = foregroundExecutor; mBackgroundExecutor = backgroundExecutor; mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); mSeekBarObserver = new SeekBarObserver(getView()); @@ -90,47 +94,101 @@ public class QSMediaPlayer extends MediaControlPanel { } /** + * Add a media panel view based on a media description. Used for resumption + * @param description + * @param iconColor + * @param bgColor + * @param contentIntent + * @param pkgName + */ + public void setMediaSession(MediaSession.Token token, MediaDescription description, + int iconColor, int bgColor, PendingIntent contentIntent, String pkgName) { + mPackageName = pkgName; + PackageManager pm = getContext().getPackageManager(); + Drawable icon = null; + CharSequence appName = pkgName.substring(pkgName.lastIndexOf(".")); + try { + icon = pm.getApplicationIcon(pkgName); + appName = pm.getApplicationLabel(pm.getApplicationInfo(pkgName, 0)); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Error getting package information", e); + } + + // Set what we can normally + super.setMediaSession(token, icon, iconColor, bgColor, contentIntent, appName.toString(), + null); + + // Then add info from MediaDescription + ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); + if (albumView != null) { + // Resize art in a background thread + mBackgroundExecutor.execute(() -> processAlbumArt(description, albumView)); + } + + // Song name + TextView titleText = mMediaNotifView.findViewById(R.id.header_title); + CharSequence songName = description.getTitle(); + titleText.setText(songName); + titleText.setTextColor(iconColor); + + // Artist name (not in mini player) + TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); + if (artistText != null) { + CharSequence artistName = description.getSubtitle(); + artistText.setText(artistName); + artistText.setTextColor(iconColor); + } + + initLongPressMenu(iconColor); + + // Set buttons to resume state + resetButtons(); + } + + /** * Update media panel view for the given media session * @param token token for this media session * @param icon app notification icon * @param iconColor foreground color (for text, icons) * @param bgColor background color * @param actionsContainer a LinearLayout containing the media action buttons - * @param notif reference to original notification + * @param contentIntent Intent to send when user taps on player + * @param appName Application title * @param key original notification's key */ - public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, - int bgColor, View actionsContainer, Notification notif, String key) { + public void setMediaSession(MediaSession.Token token, Drawable icon, int iconColor, + int bgColor, View actionsContainer, PendingIntent contentIntent, String appName, + String key) { - String appName = Notification.Builder.recoverBuilder(getContext(), notif) - .loadHeaderAppName(); - super.setMediaSession(token, icon, iconColor, bgColor, notif.contentIntent, appName, key); + super.setMediaSession(token, icon, iconColor, bgColor, contentIntent, appName, key); // Media controls - LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; - int i = 0; - for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); - ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]); - if (thatBtn == null || thatBtn.getDrawable() == null - || thatBtn.getVisibility() != View.VISIBLE) { - thisBtn.setVisibility(View.GONE); - continue; - } + if (actionsContainer != null) { + LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; + int i = 0; + for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) { + ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); + ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]); + if (thatBtn == null || thatBtn.getDrawable() == null + || thatBtn.getVisibility() != View.VISIBLE) { + thisBtn.setVisibility(View.GONE); + continue; + } - Drawable thatIcon = thatBtn.getDrawable(); - thisBtn.setImageDrawable(thatIcon.mutate()); - thisBtn.setVisibility(View.VISIBLE); - thisBtn.setOnClickListener(v -> { - Log.d(TAG, "clicking on other button"); - thatBtn.performClick(); - }); - } + Drawable thatIcon = thatBtn.getDrawable(); + thisBtn.setImageDrawable(thatIcon.mutate()); + thisBtn.setVisibility(View.VISIBLE); + thisBtn.setOnClickListener(v -> { + Log.d(TAG, "clicking on other button"); + thatBtn.performClick(); + }); + } - // Hide any unused buttons - for (; i < QS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); - thisBtn.setVisibility(View.GONE); + // Hide any unused buttons + for (; i < QS_ACTION_IDS.length; i++) { + ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); + thisBtn.setVisibility(View.GONE); + } } // Seek Bar @@ -138,6 +196,10 @@ public class QSMediaPlayer extends MediaControlPanel { mBackgroundExecutor.execute( () -> mSeekBarViewModel.updateController(controller, iconColor)); + initLongPressMenu(iconColor); + } + + private void initLongPressMenu(int iconColor) { // Set up long press menu View guts = mMediaNotifView.findViewById(R.id.media_guts); View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); @@ -145,7 +207,7 @@ public class QSMediaPlayer extends MediaControlPanel { View clearView = options.findViewById(R.id.remove); clearView.setOnClickListener(b -> { - mParent.removeMediaPlayer(QSMediaPlayer.this); + removePlayer(); }); ImageView removeIcon = options.findViewById(R.id.remove_icon); removeIcon.setImageTintList(ColorStateList.valueOf(iconColor)); @@ -165,11 +227,9 @@ public class QSMediaPlayer extends MediaControlPanel { } @Override - public void clearControls() { - super.clearControls(); - + protected void resetButtons() { + super.resetButtons(); mSeekBarViewModel.clearController(); - View guts = mMediaNotifView.findViewById(R.id.media_guts); View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); @@ -192,4 +252,19 @@ public class QSMediaPlayer extends MediaControlPanel { public void setListening(boolean listening) { mSeekBarViewModel.setListening(listening); } + + @Override + public void removePlayer() { + Log.d(TAG, "removing player from parent: " + mParent); + // Ensure this happens on the main thread (could happen in QSMediaBrowser callback) + mForegroundExecutor.execute(() -> mParent.removeMediaPlayer(QSMediaPlayer.this)); + } + + @Override + public String getMediaPlayerPackage() { + if (getController() == null) { + return mPackageName; + } + return super.getMediaPlayerPackage(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index fee08389388d..1eb577852a71 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -21,16 +21,25 @@ import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEX import static com.android.systemui.util.Utils.useQsMediaPlayer; import android.annotation.Nullable; +import android.app.Notification; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.drawable.Icon; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.media.MediaDescription; import android.media.session.MediaSession; import android.metrics.LogMaker; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.UserHandle; +import android.os.UserManager; import android.service.notification.StatusBarNotification; import android.service.quicksettings.Tile; import android.util.AttributeSet; @@ -54,6 +63,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.media.MediaControlPanel; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTileView; @@ -90,6 +100,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne protected final Context mContext; protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); + private final BroadcastDispatcher mBroadcastDispatcher; private String mCachedSpecs = ""; protected final View mBrightnessView; private final H mHandler = new H(); @@ -123,6 +134,19 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne private BrightnessMirrorController mBrightnessMirrorController; private View mDivider; + private boolean mHasLoadedMediaControls; + + private final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_USER_UNLOCKED.equals(action)) { + if (!mHasLoadedMediaControls) { + loadMediaResumptionControls(); + } + } + } + }; @Inject public QSPanel( @@ -142,6 +166,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mForegroundExecutor = foregroundExecutor; mBackgroundExecutor = backgroundExecutor; mLocalBluetoothManager = localBluetoothManager; + mBroadcastDispatcher = broadcastDispatcher; setOrientation(VERTICAL); @@ -176,7 +201,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne updateResources(); mBrightnessController = new BrightnessController(getContext(), - findViewById(R.id.brightness_slider), broadcastDispatcher); + findViewById(R.id.brightness_slider), mBroadcastDispatcher); } @Override @@ -206,7 +231,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne * @param notif * @param key */ - public void addMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor, + public void addMediaSession(MediaSession.Token token, Drawable icon, int iconColor, int bgColor, View actionsContainer, StatusBarNotification notif, String key) { if (!useQsMediaPlayer(mContext)) { // Shouldn't happen, but just in case @@ -221,7 +246,14 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne QSMediaPlayer player = null; String packageName = notif.getPackageName(); for (QSMediaPlayer p : mMediaPlayers) { - if (p.getMediaSessionToken().equals(token)) { + if (p.getKey() == null) { + // No notification key = loaded via mediabrowser, so just match on package + if (packageName.equals(p.getMediaPlayerPackage())) { + Log.d(TAG, "Found matching resume player by package: " + packageName); + player = p; + break; + } + } else if (p.getMediaSessionToken().equals(token)) { Log.d(TAG, "Found matching player by token " + packageName); player = p; break; @@ -262,8 +294,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } Log.d(TAG, "setting player session"); + String appName = Notification.Builder.recoverBuilder(getContext(), notif.getNotification()) + .loadHeaderAppName(); player.setMediaSession(token, icon, iconColor, bgColor, actionsContainer, - notif.getNotification(), key); + notif.getNotification().contentIntent, appName, key); if (mMediaPlayers.size() > 0) { ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE); @@ -293,6 +327,74 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne return true; } + private final QSMediaBrowser.Callback mMediaBrowserCallback = new QSMediaBrowser.Callback() { + @Override + public void addTrack(MediaDescription desc, ComponentName component, + QSMediaBrowser browser) { + if (component == null) { + Log.e(TAG, "Component cannot be null"); + return; + } + + Log.d(TAG, "adding track from browser: " + desc + ", " + component); + QSMediaPlayer player = new QSMediaPlayer(mContext, QSPanel.this, + null, mForegroundExecutor, mBackgroundExecutor); + + String pkgName = component.getPackageName(); + + // Add controls to carousel + int playerWidth = (int) getResources().getDimension(R.dimen.qs_media_width); + int padding = (int) getResources().getDimension(R.dimen.qs_media_padding); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(playerWidth, + LayoutParams.MATCH_PARENT); + lp.setMarginStart(padding); + lp.setMarginEnd(padding); + mMediaCarousel.addView(player.getView(), lp); + ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE); + mMediaPlayers.add(player); + + int iconColor = Color.DKGRAY; + int bgColor = Color.LTGRAY; + + MediaSession.Token token = browser.getToken(); + player.setMediaSession(token, desc, iconColor, bgColor, browser.getAppIntent(), + pkgName); + } + }; + + /** + * Load controls for resuming media, if available + */ + private void loadMediaResumptionControls() { + if (!useQsMediaPlayer(mContext)) { + return; + } + Log.d(TAG, "Loading resumption controls"); + + // Look up saved components to resume + Context userContext = mContext.createContextAsUser(mContext.getUser(), 0); + SharedPreferences prefs = userContext.getSharedPreferences( + MediaControlPanel.MEDIA_PREFERENCES, Context.MODE_PRIVATE); + String listString = prefs.getString(MediaControlPanel.MEDIA_PREFERENCE_KEY, null); + if (listString == null) { + Log.d(TAG, "No saved media components"); + return; + } + + String[] components = listString.split(QSMediaBrowser.DELIMITER); + Log.d(TAG, "components are: " + listString + " count " + components.length); + for (int i = 0; i < components.length && i < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) { + String[] info = components[i].split("/"); + String packageName = info[0]; + String className = info[1]; + ComponentName component = new ComponentName(packageName, className); + QSMediaBrowser browser = new QSMediaBrowser(mContext, mMediaBrowserCallback, + component); + browser.findRecentMedia(); + } + mHasLoadedMediaControls = true; + } + protected void addDivider() { mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false); mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(), @@ -343,6 +445,22 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mBrightnessMirrorController.addCallback(this); } mDumpManager.registerDumpable(getDumpableTag(), this); + + if (getClass() == QSPanel.class) { + //TODO(ethibodeau) remove class check after media refactor in ag/11059751 + // Only run this in QSPanel proper, not QQS + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_USER_UNLOCKED); + mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, filter, null, + UserHandle.ALL); + mHasLoadedMediaControls = false; + + UserManager userManager = mContext.getSystemService(UserManager.class); + if (userManager.isUserUnlocked(mContext.getUserId())) { + // If it's already unlocked (like if dark theme was toggled), we can load now + loadMediaResumptionControls(); + } + } } @Override @@ -358,6 +476,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mBrightnessMirrorController.removeCallback(this); } mDumpManager.unregisterDumpable(getDumpableTag()); + mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); super.onDetachedFromWindow(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java index 62296720213b..7ba7c5fe499e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java @@ -19,7 +19,6 @@ package com.android.systemui.qs; import android.app.PendingIntent; import android.content.Context; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; import android.media.session.MediaController; import android.media.session.MediaSession; import android.view.View; @@ -67,7 +66,7 @@ public class QuickQSMediaPlayer extends MediaControlPanel { * @param contentIntent Intent to send when user taps on the view * @param key original notification's key */ - public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor, + public void setMediaSession(MediaSession.Token token, Drawable icon, int iconColor, int bgColor, View actionsContainer, int[] actionsToShow, PendingIntent contentIntent, String key) { // Only update if this is a different session and currently playing String oldPackage = ""; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java new file mode 100644 index 000000000000..5ced40cb1b3b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 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.screenshot; + +import android.os.IBinder; +import android.view.IWindowManager; + +import javax.inject.Inject; + +/** + * Stub + */ +public class ScrollCaptureController { + + public static final int STATUS_A = 0; + public static final int STATUS_B = 1; + + private final IWindowManager mWindowManagerService; + private StatusListener mListener; + + /** + * + * @param windowManagerService + */ + @Inject + public ScrollCaptureController(IWindowManager windowManagerService) { + mWindowManagerService = windowManagerService; + } + + interface StatusListener { + void onScrollCaptureStatus(boolean available); + } + + /** + * + * @param window + * @param listener + */ + public void getStatus(IBinder window, StatusListener listener) { + mListener = listener; +// try { +// mWindowManagerService.requestScrollCapture(window, new ClientCallbacks()); +// } catch (RemoteException e) { +// } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt index fa1b0267fafa..4de978c77128 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt @@ -18,8 +18,10 @@ package com.android.systemui.settings import android.content.Context import android.os.UserHandle +import androidx.annotation.VisibleForTesting import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.util.Assert +import java.lang.IllegalStateException import javax.inject.Inject import javax.inject.Singleton @@ -32,7 +34,16 @@ class CurrentUserContextTracker @Inject constructor( broadcastDispatcher: BroadcastDispatcher ) { private val userTracker: CurrentUserTracker - var currentUserContext: Context + private var initialized = false + + private var _curUserContext: Context? = null + val currentUserContext: Context + get() { + if (!initialized) { + throw IllegalStateException("Must initialize before getting context") + } + return _curUserContext!! + } init { userTracker = object : CurrentUserTracker(broadcastDispatcher) { @@ -40,21 +51,21 @@ class CurrentUserContextTracker @Inject constructor( handleUserSwitched(newUserId) } } - - currentUserContext = makeUserContext(userTracker.currentUserId) } fun initialize() { + initialized = true + _curUserContext = makeUserContext(userTracker.currentUserId) userTracker.startTracking() } - private fun handleUserSwitched(newUserId: Int) { - currentUserContext = makeUserContext(newUserId) + @VisibleForTesting + fun handleUserSwitched(newUserId: Int) { + _curUserContext = makeUserContext(newUserId) } private fun makeUserContext(uid: Int): Context { Assert.isMainThread() - return sysuiContext.createContextAsUser( - UserHandle.getUserHandleForUid(userTracker.currentUserId), 0) + return sysuiContext.createContextAsUser(UserHandle.of(uid), 0) } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java index 85dcbb6316d0..5aa7946bcb7f 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java @@ -180,12 +180,17 @@ public class WindowManagerProxy { if (isHomeOrRecentTask(rootTask)) { tiles.mHomeAndRecentsSurfaces.add(rootTask.token.getLeash()); } + // Only move resizeable task to split secondary. WM will just ignore this anyways... + if (!rootTask.isResizable()) continue; + // Only move fullscreen tasks to split secondary. if (rootTask.configuration.windowConfiguration.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { continue; } wct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */); } + // Move the secondary split-forward. + wct.reorder(tiles.mSecondary.token, true /* onTop */); boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, wct); WindowOrganizer.applyTransaction(wct); return isHomeResizable; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 37fc13e2df5b..afb50027f03d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -123,7 +123,6 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications); mBubbleController = bubbleController; mDynamicPrivacyController = privacyController; - privacyController.addListener(this); mDynamicChildBindController = dynamicChildBindController; } @@ -131,6 +130,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle NotificationListContainer listContainer) { mPresenter = presenter; mListContainer = listContainer; + mDynamicPrivacyController.addListener(this); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 5236385b3716..cb0c2838c24d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -378,6 +378,7 @@ public final class NotificationEntry extends ListEntry { /** * Returns the data needed for a bubble for this notification, if it exists. */ + @Nullable public Notification.BubbleMetadata getBubbleMetadata() { return mBubbleMetadata; } @@ -385,7 +386,7 @@ public final class NotificationEntry extends ListEntry { /** * Sets bubble metadata for this notification. */ - public void setBubbleMetadata(Notification.BubbleMetadata metadata) { + public void setBubbleMetadata(@Nullable Notification.BubbleMetadata metadata) { mBubbleMetadata = metadata; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java index 6d14ccf85716..9b6ae9a7f99d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java @@ -175,7 +175,7 @@ public class HeadsUpController { private OnHeadsUpChangedListener mOnHeadsUpChangedListener = new OnHeadsUpChangedListener() { @Override public void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) { - if (!isHeadsUp) { + if (!isHeadsUp && !entry.getRow().isRemoved()) { mHeadsUpViewBinder.unbindHeadsUpView(entry); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 55a593541819..bcc81a8b967f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -31,6 +31,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; @@ -98,7 +99,7 @@ public class NotificationConversationInfo extends LinearLayout implements private ShortcutInfo mShortcutInfo; private String mConversationId; private StatusBarNotification mSbn; - private Notification.BubbleMetadata mBubbleMetadata; + @Nullable private Notification.BubbleMetadata mBubbleMetadata; private Context mUserContext; private Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider; private boolean mIsDeviceProvisioned; @@ -203,6 +204,7 @@ public class NotificationConversationInfo extends LinearLayout implements String pkg, NotificationChannel notificationChannel, NotificationEntry entry, + Notification.BubbleMetadata bubbleMetadata, OnSettingsClickListener onSettingsClick, OnSnoozeClickListener onSnoozeClickListener, ConversationIconFactory conversationIconFactory, @@ -224,7 +226,7 @@ public class NotificationConversationInfo extends LinearLayout implements mOnSnoozeClickListener = onSnoozeClickListener; mIconFactory = conversationIconFactory; mUserContext = userContext; - mBubbleMetadata = entry.getBubbleMetadata(); + mBubbleMetadata = bubbleMetadata; mBuilderProvider = builderProvider; mShortcutManager = shortcutManager; @@ -538,7 +540,8 @@ public class NotificationConversationInfo extends LinearLayout implements Log.e(TAG, "Could not check conversation senders", e); } - boolean showAsBubble = mBubbleMetadata.getAutoExpandBubble() + boolean showAsBubble = mBubbleMetadata != null + && mBubbleMetadata.getAutoExpandBubble() && Settings.Global.getInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 0) == 1; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 624fabc0a496..1c808cf90321 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -366,7 +366,8 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx final ExpandableNotificationRow row, NotificationConversationInfo notificationInfoView) throws Exception { NotificationGuts guts = row.getGuts(); - StatusBarNotification sbn = row.getEntry().getSbn(); + NotificationEntry entry = row.getEntry(); + StatusBarNotification sbn = entry.getSbn(); String packageName = sbn.getPackageName(); // Settings link is only valid for notifications that specify a non-system user NotificationConversationInfo.OnSettingsClickListener onSettingsClick = null; @@ -407,8 +408,9 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx mNotificationManager, mVisualStabilityManager, packageName, - row.getEntry().getChannel(), - row.getEntry(), + entry.getChannel(), + entry, + entry.getBubbleMetadata(), onSettingsClick, onSnoozeClickListener, iconFactoryLoader, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java index 2da2724aacb2..796f22cbb3cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.app.Notification; import android.content.Context; import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaSession; @@ -187,8 +188,9 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi com.android.systemui.R.id.quick_qs_panel); StatusBarNotification sbn = mRow.getEntry().getSbn(); Notification notif = sbn.getNotification(); + Drawable iconDrawable = notif.getSmallIcon().loadDrawable(mContext); panel.getMediaPlayer().setMediaSession(token, - notif.getSmallIcon(), + iconDrawable, tintColor, mBackgroundColor, mActions, @@ -198,7 +200,7 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi QSPanel bigPanel = ctrl.getNotificationShadeView().findViewById( com.android.systemui.R.id.quick_settings_panel); bigPanel.addMediaSession(token, - notif.getSmallIcon(), + iconDrawable, tintColor, mBackgroundColor, mActions, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index 82e02b47974c..39949c82661f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -400,6 +400,9 @@ public class KeyguardBouncer { mExpansionCallback.onFullyHidden(); } else if (fraction != EXPANSION_VISIBLE && oldExpansion == EXPANSION_VISIBLE) { mExpansionCallback.onStartingToHide(); + if (mKeyguardView != null) { + mKeyguardView.onStartingToHide(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java b/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java index e5da603321cd..899aabb2e9a7 100644 --- a/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java +++ b/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java @@ -32,6 +32,7 @@ import android.util.SparseArray; import android.view.Display; import android.view.DisplayCutout; import android.view.DragEvent; +import android.view.IScrollCaptureController; import android.view.IWindow; import android.view.IWindowManager; import android.view.IWindowSession; @@ -352,5 +353,14 @@ public class SystemWindows { @Override public void dispatchPointerCaptureChanged(boolean hasCapture) {} + + @Override + public void requestScrollCapture(IScrollCaptureController controller) { + try { + controller.onClientUnavailable(); + } catch (RemoteException ex) { + // ignore + } + } } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java index 25f279b45d04..dd5c8335eefa 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java @@ -17,30 +17,50 @@ package com.android.keyguard; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import com.android.internal.widget.LockPatternUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper public class KeyguardHostViewTest extends SysuiTestCase { + @Mock + private KeyguardSecurityContainer mSecurityContainer; + @Mock + private LockPatternUtils mLockPatternUtils; + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + private KeyguardHostView mKeyguardHostView; @Before public void setup() { mDependency.injectMockDependency(KeyguardUpdateMonitor.class); - mKeyguardHostView = new KeyguardHostView(getContext()); + mKeyguardHostView = new KeyguardHostView(getContext()) { + @Override + protected void onFinishInflate() { + mSecurityContainer = KeyguardHostViewTest.this.mSecurityContainer; + mLockPatternUtils = KeyguardHostViewTest.this.mLockPatternUtils; + } + }; + mKeyguardHostView.onFinishInflate(); } @Test @@ -50,4 +70,10 @@ public class KeyguardHostViewTest extends SysuiTestCase { null /* cancelAction */); Assert.assertTrue("Action should exist", mKeyguardHostView.hasDismissActions()); } + + @Test + public void testOnStartingToHide() { + mKeyguardHostView.onStartingToHide(); + verify(mSecurityContainer).onStartingToHide(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserContextTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserContextTrackerTest.kt new file mode 100644 index 000000000000..628c06a56abd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserContextTrackerTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2020 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.settings + +import android.content.Context +import android.content.ContextWrapper +import android.os.UserHandle +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class CurrentUserContextTrackerTest : SysuiTestCase() { + + private lateinit var tracker: CurrentUserContextTracker + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + allowTestableLooperAsMainThread() + + // wrap Context so that tests don't throw for missing package errors + val wrapped = object : ContextWrapper(context) { + override fun createContextAsUser(user: UserHandle, flags: Int): Context { + val mockContext = mock(Context::class.java) + `when`(mockContext.user).thenReturn(user) + `when`(mockContext.userId).thenReturn(user.identifier) + return mockContext + } + } + + tracker = CurrentUserContextTracker(wrapped, broadcastDispatcher) + tracker.initialize() + } + + @Test + fun testContextExistsAfterInit_noCrash() { + tracker.currentUserContext + } + + @Test + fun testUserContextIsCorrectAfterUserSwitch() { + // We always start out with system ui test + assertTrue("Starting userId should be 0", tracker.currentUserContext.userId == 0) + + // WHEN user changes + tracker.handleUserSwitched(1) + + // THEN user context should have the correct userId + assertTrue("User has changed to userId 1, the context should reflect that", + tracker.currentUserContext.userId == 1) + } + + @Suppress("UNUSED_PARAMETER") + @Test(expected = IllegalStateException::class) + fun testContextTrackerThrowsExceptionWhenNotInitialized() { + // GIVEN an uninitialized CurrentUserContextTracker + val userTracker = CurrentUserContextTracker(context, broadcastDispatcher) + + // WHEN client asks for a context + val userContext = userTracker.currentUserContext + + // THEN an exception is thrown + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java index 61388b6d0389..6db868563d3d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java @@ -48,9 +48,9 @@ import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; +import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Person; -import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; @@ -91,6 +91,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; @@ -147,14 +148,15 @@ public class NotificationConversationInfoTest extends SysuiTestCase { private ShadeController mShadeController; @Mock private ConversationIconFactory mIconFactory; - @Mock - private Context mUserContext; @Mock(answer = Answers.RETURNS_SELF) private PriorityOnboardingDialogController.Builder mBuilder; private Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider = () -> mBuilder; + @Mock + private Notification.BubbleMetadata mBubbleMetadata; @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); mTestableLooper = TestableLooper.get(this); mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper()); @@ -228,6 +230,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { when(mMockINotificationManager.getConversationNotificationChannel(anyString(), anyInt(), anyString(), eq(TEST_CHANNEL), eq(false), eq(CONVERSATION_ID))) .thenReturn(mConversationChannel); + + when(mMockINotificationManager.getConsolidatedNotificationPolicy()) + .thenReturn(mock(NotificationManager.Policy.class)); + + when(mBuilder.build()).thenReturn(mock(PriorityOnboardingDialogController.class)); } @Test @@ -240,10 +247,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final ImageView view = mNotificationInfo.findViewById(R.id.conversation_icon); @@ -261,10 +269,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name); @@ -283,7 +292,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, - null, + mBubbleMetadata, + null, null, null, true); @@ -308,10 +318,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final TextView textView = mNotificationInfo.findViewById(R.id.group_name); @@ -331,10 +342,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final TextView textView = mNotificationInfo.findViewById(R.id.group_name); @@ -353,10 +365,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name); @@ -382,10 +395,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, entry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name); @@ -404,13 +418,14 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, (View v, NotificationChannel c, int appUid) -> { assertEquals(mConversationChannel, c); latch.countDown(); }, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -430,10 +445,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final View settingsButton = mNotificationInfo.findViewById(R.id.info); @@ -451,13 +467,14 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, (View v, NotificationChannel c, int appUid) -> { assertEquals(mNotificationChannel, c); latch.countDown(); }, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, false); final View settingsButton = mNotificationInfo.findViewById(R.id.info); @@ -476,10 +493,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); View view = mNotificationInfo.findViewById(R.id.silence); @@ -501,10 +519,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); View view = mNotificationInfo.findViewById(R.id.default_behavior); @@ -529,10 +548,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); View view = mNotificationInfo.findViewById(R.id.default_behavior); @@ -556,10 +576,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -596,10 +617,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -635,10 +657,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -675,10 +698,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -709,10 +733,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -741,10 +766,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -774,10 +800,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -807,10 +834,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -839,10 +867,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -870,10 +899,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -892,10 +922,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -910,13 +941,12 @@ public class NotificationConversationInfoTest extends SysuiTestCase { // GIVEN the priority onboarding screen is present PriorityOnboardingDialogController.Builder b = - new PriorityOnboardingDialogController.Builder(); + mock(PriorityOnboardingDialogController.Builder.class, Answers.RETURNS_SELF); PriorityOnboardingDialogController controller = mock(PriorityOnboardingDialogController.class); when(b.build()).thenReturn(controller); // GIVEN the user is changing conversation settings - when(mBuilderProvider.get()).thenReturn(b); mNotificationInfo.bindNotification( mShortcutManager, mMockPackageManager, @@ -925,11 +955,12 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, - mBuilderProvider, + mContext, + () -> b, true); // WHEN user clicks "priority" @@ -945,12 +976,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, true); PriorityOnboardingDialogController.Builder b = - new PriorityOnboardingDialogController.Builder(); + mock(PriorityOnboardingDialogController.Builder.class, Answers.RETURNS_SELF); PriorityOnboardingDialogController controller = mock(PriorityOnboardingDialogController.class); when(b.build()).thenReturn(controller); - when(mBuilderProvider.get()).thenReturn(b); mNotificationInfo.bindNotification( mShortcutManager, mMockPackageManager, @@ -959,11 +989,12 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, - mBuilderProvider, + mContext, + () -> b, true); // WHEN user clicks "priority" diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java index e052ae2653f0..0a041e4a4dc5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java @@ -26,7 +26,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; @@ -37,6 +36,7 @@ import android.graphics.Color; import android.os.Handler; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; @@ -64,6 +64,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -94,9 +95,11 @@ public class KeyguardBouncerTest extends SysuiTestCase { private Handler mHandler; @Mock private KeyguardSecurityModel mKeyguardSecurityModel; + @Mock + private ViewGroup mRootView; @Rule public MockitoRule mRule = MockitoJUnit.rule(); - private ViewGroup mRootView; + private Integer mRootVisibility = View.INVISIBLE; private KeyguardBouncer mBouncer; @Before @@ -105,6 +108,11 @@ public class KeyguardBouncerTest extends SysuiTestCase { mDependency.injectTestDependency(KeyguardUpdateMonitor.class, mKeyguardUpdateMonitor); mDependency.injectTestDependency(KeyguardSecurityModel.class, mKeyguardSecurityModel); mDependency.injectMockDependency(KeyguardStateController.class); + when(mRootView.getVisibility()).thenAnswer((Answer<Integer>) invocation -> mRootVisibility); + doAnswer(invocation -> { + mRootVisibility = invocation.getArgument(0); + return null; + }).when(mRootView).setVisibility(anyInt()); when(mKeyguardSecurityModel.getSecurityMode(anyInt())) .thenReturn(KeyguardSecurityModel.SecurityMode.None); DejankUtils.setImmediate(true); @@ -117,10 +125,8 @@ public class KeyguardBouncerTest extends SysuiTestCase { mKeyguardBypassController, mHandler) { @Override protected void inflateView() { - super.inflateView(); mKeyguardView = mKeyguardHostView; - mRoot = spy(mRoot); - mRootView = mRoot; + mRoot = mRootView; } }; } @@ -212,8 +218,10 @@ public class KeyguardBouncerTest extends SysuiTestCase { verify(mExpansionCallback).onFullyShown(); verify(mExpansionCallback, never()).onStartingToHide(); + verify(mKeyguardHostView, never()).onStartingToHide(); mBouncer.setExpansion(0.9f); verify(mExpansionCallback).onStartingToHide(); + verify(mKeyguardHostView).onStartingToHide(); } @Test diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index f21f0e73e787..1a72cf023453 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -2732,7 +2732,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) { - new AccessibilityShellCommand(this).exec(this, in, out, err, args, + new AccessibilityShellCommand(this, mSystemActionPerformer).exec(this, in, out, err, args, callback, resultReceiver); } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java index 20a11bd9acd3..b36626f9d736 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java @@ -18,6 +18,8 @@ package com.android.server.accessibility; import android.annotation.NonNull; import android.app.ActivityManager; +import android.os.Binder; +import android.os.Process; import android.os.ShellCommand; import android.os.UserHandle; @@ -28,9 +30,12 @@ import java.io.PrintWriter; */ final class AccessibilityShellCommand extends ShellCommand { final @NonNull AccessibilityManagerService mService; + final @NonNull SystemActionPerformer mSystemActionPerformer; - AccessibilityShellCommand(@NonNull AccessibilityManagerService service) { + AccessibilityShellCommand(@NonNull AccessibilityManagerService service, + @NonNull SystemActionPerformer systemActionPerformer) { mService = service; + mSystemActionPerformer = systemActionPerformer; } @Override @@ -45,6 +50,9 @@ final class AccessibilityShellCommand extends ShellCommand { case "set-bind-instant-service-allowed": { return runSetBindInstantServiceAllowed(); } + case "call-system-action": { + return runCallSystemAction(); + } } return -1; } @@ -74,6 +82,22 @@ final class AccessibilityShellCommand extends ShellCommand { return 0; } + private int runCallSystemAction() { + final int callingUid = Binder.getCallingUid(); + if (callingUid != Process.ROOT_UID + && callingUid != Process.SYSTEM_UID + && callingUid != Process.SHELL_UID) { + return -1; + } + final String option = getNextArg(); + if (option != null) { + int actionId = Integer.parseInt(option); + mSystemActionPerformer.performSystemAction(actionId); + return 0; + } + return -1; + } + private Integer parseUserId() { final String option = getNextOption(); if (option != null) { @@ -97,5 +121,7 @@ final class AccessibilityShellCommand extends ShellCommand { pw.println(" Set whether binding to services provided by instant apps is allowed."); pw.println(" get-bind-instant-service-allowed [--user <USER_ID>]"); pw.println(" Get whether binding to services provided by instant apps is allowed."); + pw.println(" call-system-action <ACTION_ID>"); + pw.println(" Calls the system action with the given action id."); } }
\ No newline at end of file diff --git a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java index ef8d524bee25..b9e3050275ad 100644 --- a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java +++ b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java @@ -301,7 +301,11 @@ public class SystemActionPerformer { return lockScreen(); case AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT: return takeScreenshot(); + case AccessibilityService.GLOBAL_ACTION_KEYCODE_HEADSETHOOK : + sendDownAndUpKeyEvents(KeyEvent.KEYCODE_HEADSETHOOK); + return true; default: + Slog.e(TAG, "Invalid action id: " + actionId); return false; } } finally { diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 9d1ad4239a24..b27c5d54a6fb 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -717,10 +717,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Consumer<InlineSuggestionsRequest> inlineSuggestionsRequestConsumer = mAssistReceiver.newAutofillRequestLocked(/*isInlineRequest=*/ true); if (inlineSuggestionsRequestConsumer != null) { + final AutofillId focusedId = mCurrentViewId; remoteRenderService.getInlineSuggestionsRendererInfo( new RemoteCallback((extras) -> { mInlineSessionController.onCreateInlineSuggestionsRequestLocked( - mCurrentViewId, inlineSuggestionsRequestConsumer, extras); + focusedId, inlineSuggestionsRequestConsumer, extras); } )); } @@ -2786,6 +2787,12 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState */ private boolean requestShowInlineSuggestionsLocked(@NonNull FillResponse response, @Nullable String filterText) { + if (mCurrentViewId == null) { + Log.w(TAG, "requestShowInlineSuggestionsLocked(): no view currently focused"); + return false; + } + final AutofillId focusedId = mCurrentViewId; + final Optional<InlineSuggestionsRequest> inlineSuggestionsRequest = mInlineSessionController.getInlineSuggestionsRequestLocked(); if (!inlineSuggestionsRequest.isPresent()) { @@ -2800,17 +2807,17 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return false; } - final ViewState currentView = mViewStates.get(mCurrentViewId); + final ViewState currentView = mViewStates.get(focusedId); if ((currentView.getState() & ViewState.STATE_INLINE_DISABLED) != 0) { response.getDatasets().clear(); } InlineSuggestionsResponse inlineSuggestionsResponse = InlineSuggestionFactory.createInlineSuggestionsResponse( - inlineSuggestionsRequest.get(), response, filterText, mCurrentViewId, + inlineSuggestionsRequest.get(), response, filterText, focusedId, this, () -> { synchronized (mLock) { mInlineSessionController.hideInlineSuggestionsUiLocked( - mCurrentViewId); + focusedId); } }, remoteRenderService); if (inlineSuggestionsResponse == null) { @@ -2818,7 +2825,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return false; } - return mInlineSessionController.onInlineSuggestionsResponseLocked(mCurrentViewId, + return mInlineSessionController.onInlineSuggestionsResponseLocked(focusedId, inlineSuggestionsResponse); } @@ -3107,19 +3114,19 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState remoteService.getComponentName().getPackageName()); mAugmentedRequestsLogs.add(log); - final AutofillId focusedId = AutofillId.withoutSession(mCurrentViewId); + final AutofillId focusedId = mCurrentViewId; final Consumer<InlineSuggestionsRequest> requestAugmentedAutofill = (inlineSuggestionsRequest) -> { remoteService.onRequestAutofillLocked(id, mClient, taskId, mComponentName, - focusedId, + AutofillId.withoutSession(focusedId), currentValue, inlineSuggestionsRequest, /*inlineSuggestionsCallback=*/ response -> { synchronized (mLock) { return mInlineSessionController .onInlineSuggestionsResponseLocked( - mCurrentViewId, response); + focusedId, response); } }, /*onErrorCallback=*/ () -> { @@ -3144,7 +3151,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState remoteRenderService.getInlineSuggestionsRendererInfo(new RemoteCallback( (extras) -> { mInlineSessionController.onCreateInlineSuggestionsRequestLocked( - mCurrentViewId, /*requestConsumer=*/ requestAugmentedAutofill, + focusedId, /*requestConsumer=*/ requestAugmentedAutofill, extras); }, mHandler)); } else { diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java index c27ec66b5db3..63b56e0f92d4 100644 --- a/services/core/java/android/content/pm/PackageManagerInternal.java +++ b/services/core/java/android/content/pm/PackageManagerInternal.java @@ -480,6 +480,12 @@ public abstract class PackageManagerInternal { public abstract void pruneInstantApps(); /** + * Prunes the cache of the APKs in the given APEXes. + * @param apexPackages The list of APEX packages that may contain APK-in-APEX. + */ + public abstract void pruneCachedApksInApex(@NonNull List<PackageInfo> apexPackages); + + /** * @return The SetupWizard package name. */ public abstract String getSetupWizardPackageName(); diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java index ac4a42ca7024..e066d99147ba 100644 --- a/services/core/java/com/android/server/VibratorService.java +++ b/services/core/java/com/android/server/VibratorService.java @@ -1034,6 +1034,9 @@ public class VibratorService extends IVibratorService.Stub VibrationEffect.Waveform waveform = (VibrationEffect.Waveform) vib.effect; waveform = waveform.resolve(mDefaultVibrationAmplitude); scaledEffect = waveform.scale(scale.gamma, scale.maxAmplitude); + } else if (vib.effect instanceof VibrationEffect.Composed) { + VibrationEffect.Composed composed = (VibrationEffect.Composed) vib.effect; + scaledEffect = composed.scale(scale.gamma, scale.maxAmplitude); } else { Slog.w(TAG, "Unable to apply intensity scaling, unknown VibrationEffect type"); } diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 8f5fbf7431e1..149e3baa90e7 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -2504,7 +2504,7 @@ final class ActivityManagerShellCommand extends ShellCommand { IUsageStatsManager usm = IUsageStatsManager.Stub.asInterface(ServiceManager.getService( Context.USAGE_STATS_SERVICE)); - boolean isIdle = usm.isAppInactive(packageName, userId); + boolean isIdle = usm.isAppInactive(packageName, userId, SHELL_PACKAGE_NAME); pw.println("Idle=" + isIdle); return 0; } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java index fbee6f4bcbf0..848019738abe 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java @@ -26,6 +26,7 @@ import android.app.admin.DevicePolicyManager; import android.app.trust.IStrongAuthTracker; import android.content.Context; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -36,6 +37,7 @@ import android.util.Slog; import android.util.SparseBooleanArray; import android.util.SparseIntArray; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.widget.LockPatternUtils.StrongAuthTracker; @@ -57,11 +59,14 @@ public class LockSettingsStrongAuth { private static final int MSG_STRONG_BIOMETRIC_UNLOCK = 8; private static final int MSG_SCHEDULE_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT = 9; - private static final String STRONG_AUTH_TIMEOUT_ALARM_TAG = + @VisibleForTesting + protected static final String STRONG_AUTH_TIMEOUT_ALARM_TAG = "LockSettingsStrongAuth.timeoutForUser"; - private static final String NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG = + @VisibleForTesting + protected static final String NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG = "LockSettingsPrimaryAuth.nonStrongBiometricTimeoutForUser"; - private static final String NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG = + @VisibleForTesting + protected static final String NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG = "LockSettingsPrimaryAuth.nonStrongBiometricIdleTimeoutForUser"; /** @@ -73,28 +78,71 @@ public class LockSettingsStrongAuth { 4 * 60 * 60 * 1000; // 4h private final RemoteCallbackList<IStrongAuthTracker> mTrackers = new RemoteCallbackList<>(); - private final SparseIntArray mStrongAuthForUser = new SparseIntArray(); - private final SparseBooleanArray mIsNonStrongBiometricAllowedForUser = new SparseBooleanArray(); - private final ArrayMap<Integer, StrongAuthTimeoutAlarmListener> + @VisibleForTesting + protected final SparseIntArray mStrongAuthForUser = new SparseIntArray(); + @VisibleForTesting + protected final SparseBooleanArray mIsNonStrongBiometricAllowedForUser = + new SparseBooleanArray(); + @VisibleForTesting + protected final ArrayMap<Integer, StrongAuthTimeoutAlarmListener> mStrongAuthTimeoutAlarmListenerForUser = new ArrayMap<>(); // Track non-strong biometric timeout - private final ArrayMap<Integer, NonStrongBiometricTimeoutAlarmListener> + @VisibleForTesting + protected final ArrayMap<Integer, NonStrongBiometricTimeoutAlarmListener> mNonStrongBiometricTimeoutAlarmListener = new ArrayMap<>(); // Track non-strong biometric idle timeout - private final ArrayMap<Integer, NonStrongBiometricIdleTimeoutAlarmListener> + @VisibleForTesting + protected final ArrayMap<Integer, NonStrongBiometricIdleTimeoutAlarmListener> mNonStrongBiometricIdleTimeoutAlarmListener = new ArrayMap<>(); private final int mDefaultStrongAuthFlags; private final boolean mDefaultIsNonStrongBiometricAllowed = true; private final Context mContext; - - private AlarmManager mAlarmManager; + private final Injector mInjector; + private final AlarmManager mAlarmManager; public LockSettingsStrongAuth(Context context) { + this(context, new Injector()); + } + + @VisibleForTesting + protected LockSettingsStrongAuth(Context context, Injector injector) { mContext = context; - mDefaultStrongAuthFlags = StrongAuthTracker.getDefaultFlags(context); - mAlarmManager = context.getSystemService(AlarmManager.class); + mInjector = injector; + mDefaultStrongAuthFlags = mInjector.getDefaultStrongAuthFlags(context); + mAlarmManager = mInjector.getAlarmManager(context); + } + + /** + * Class for injecting dependencies into LockSettingsStrongAuth. + */ + @VisibleForTesting + public static class Injector { + + /** + * Allows to mock AlarmManager for testing. + */ + @VisibleForTesting + public AlarmManager getAlarmManager(Context context) { + return context.getSystemService(AlarmManager.class); + } + + /** + * Allows to get different default StrongAuthFlags for testing. + */ + @VisibleForTesting + public int getDefaultStrongAuthFlags(Context context) { + return StrongAuthTracker.getDefaultFlags(context); + } + + /** + * Allows to get different triggerAtMillis values when setting alarms for testing. + */ + @VisibleForTesting + public long getNextAlarmTimeMs(long timeout) { + return SystemClock.elapsedRealtime() + timeout; + } } private void handleAddStrongAuthTracker(IStrongAuthTracker tracker) { @@ -186,7 +234,8 @@ public class LockSettingsStrongAuth { private void handleScheduleStrongAuthTimeout(int userId) { final DevicePolicyManager dpm = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); - long when = SystemClock.elapsedRealtime() + dpm.getRequiredStrongAuthTimeout(null, userId); + long nextAlarmTime = + mInjector.getNextAlarmTimeMs(dpm.getRequiredStrongAuthTimeout(null, userId)); // cancel current alarm listener for the user (if there was one) StrongAuthTimeoutAlarmListener alarm = mStrongAuthTimeoutAlarmListenerForUser.get(userId); if (alarm != null) { @@ -196,8 +245,8 @@ public class LockSettingsStrongAuth { mStrongAuthTimeoutAlarmListenerForUser.put(userId, alarm); } // schedule a new alarm listener for the user - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, when, STRONG_AUTH_TIMEOUT_ALARM_TAG, - alarm, mHandler); + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime, + STRONG_AUTH_TIMEOUT_ALARM_TAG, alarm, mHandler); // cancel current non-strong biometric alarm listener for the user (if there was one) cancelNonStrongBiometricAlarmListener(userId); @@ -209,7 +258,7 @@ public class LockSettingsStrongAuth { private void handleScheduleNonStrongBiometricTimeout(int userId) { if (DEBUG) Slog.d(TAG, "handleScheduleNonStrongBiometricTimeout for userId=" + userId); - long when = SystemClock.elapsedRealtime() + DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS; + long nextAlarmTime = mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS); NonStrongBiometricTimeoutAlarmListener alarm = mNonStrongBiometricTimeoutAlarmListener .get(userId); if (alarm != null) { @@ -226,7 +275,7 @@ public class LockSettingsStrongAuth { alarm = new NonStrongBiometricTimeoutAlarmListener(userId); mNonStrongBiometricTimeoutAlarmListener.put(userId, alarm); // schedule a new alarm listener for the user - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, when, + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime, NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG, alarm, mHandler); } @@ -268,7 +317,8 @@ public class LockSettingsStrongAuth { } } - private void setIsNonStrongBiometricAllowed(boolean allowed, int userId) { + @VisibleForTesting + protected void setIsNonStrongBiometricAllowed(boolean allowed, int userId) { if (DEBUG) { Slog.d(TAG, "setIsNonStrongBiometricAllowed for allowed=" + allowed + ", userId=" + userId); @@ -302,7 +352,8 @@ public class LockSettingsStrongAuth { private void handleScheduleNonStrongBiometricIdleTimeout(int userId) { if (DEBUG) Slog.d(TAG, "handleScheduleNonStrongBiometricIdleTimeout for userId=" + userId); - long when = SystemClock.elapsedRealtime() + DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS; + long nextAlarmTime = + mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS); // cancel current alarm listener for the user (if there was one) NonStrongBiometricIdleTimeoutAlarmListener alarm = mNonStrongBiometricIdleTimeoutAlarmListener.get(userId); @@ -315,7 +366,7 @@ public class LockSettingsStrongAuth { } // schedule a new alarm listener for the user if (DEBUG) Slog.d(TAG, "Schedule a new alarm for non-strong biometric idle timeout"); - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, when, + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime, NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG, alarm, mHandler); } @@ -435,7 +486,8 @@ public class LockSettingsStrongAuth { /** * Alarm of fallback timeout for primary auth */ - private class StrongAuthTimeoutAlarmListener implements OnAlarmListener { + @VisibleForTesting + protected class StrongAuthTimeoutAlarmListener implements OnAlarmListener { private final int mUserId; @@ -452,7 +504,8 @@ public class LockSettingsStrongAuth { /** * Alarm of fallback timeout for non-strong biometric (i.e. weak or convenience) */ - private class NonStrongBiometricTimeoutAlarmListener implements OnAlarmListener { + @VisibleForTesting + protected class NonStrongBiometricTimeoutAlarmListener implements OnAlarmListener { private final int mUserId; @@ -469,7 +522,8 @@ public class LockSettingsStrongAuth { /** * Alarm of idle timeout for non-strong biometric (i.e. weak or convenience biometric) */ - private class NonStrongBiometricIdleTimeoutAlarmListener implements OnAlarmListener { + @VisibleForTesting + protected class NonStrongBiometricIdleTimeoutAlarmListener implements OnAlarmListener { private final int mUserId; @@ -484,7 +538,8 @@ public class LockSettingsStrongAuth { } } - private final Handler mHandler = new Handler() { + @VisibleForTesting + protected final Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java index 28f838044907..3cf22c85f924 100644 --- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java +++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java @@ -215,7 +215,6 @@ class BluetoothRouteProvider { .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED) .setDescription(mContext.getResources().getText( R.string.bluetooth_a2dp_audio_route_name).toString()) - //TODO: Set type correctly (BLUETOOTH_A2DP or HEARING_AID) .setType(MediaRoute2Info.TYPE_BLUETOOTH_A2DP) .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) .build(); @@ -236,6 +235,8 @@ class BluetoothRouteProvider { // Update volume when the connection state is changed. MediaRoute2Info.Builder builder = new MediaRoute2Info.Builder(btRoute.route) .setConnectionState(state); + builder.setType(btRoute.connectedProfiles.get(BluetoothProfile.HEARING_AID, false) + ? MediaRoute2Info.TYPE_HEARING_AID : MediaRoute2Info.TYPE_BLUETOOTH_A2DP); if (state == MediaRoute2Info.CONNECTION_STATE_CONNECTED) { int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 1345e3759d2f..41d7fff52a91 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -19,6 +19,11 @@ package com.android.server.media; import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO; import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO; import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_DOCK; +import static android.media.MediaRoute2Info.TYPE_HDMI; +import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -194,19 +199,27 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { private void updateDeviceRoute(AudioRoutesInfo newRoutes) { int name = R.string.default_audio_route_name; + int type = TYPE_BUILTIN_SPEAKER; if (newRoutes != null) { mCurAudioRoutesInfo.mainType = newRoutes.mainType; - if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0 - || (newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { + if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) { + type = TYPE_WIRED_HEADPHONES; + name = com.android.internal.R.string.default_audio_route_name_headphones; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { + type = TYPE_WIRED_HEADSET; name = com.android.internal.R.string.default_audio_route_name_headphones; } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { + type = TYPE_DOCK; name = com.android.internal.R.string.default_audio_route_name_dock_speakers; } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) { + type = TYPE_HDMI; name = com.android.internal.R.string.default_audio_route_name_hdmi; } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) { + type = TYPE_USB_DEVICE; name = com.android.internal.R.string.default_audio_route_name_usb; } } + mDeviceRoute = new MediaRoute2Info.Builder( DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString()) .setVolumeHandling(mAudioManager.isVolumeFixed() @@ -214,8 +227,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) .setVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)) - //TODO: Guess the exact type using AudioDevice - .setType(TYPE_BUILTIN_SPEAKER) + .setType(type) .addFeature(FEATURE_LIVE_AUDIO) .addFeature(FEATURE_LIVE_VIDEO) .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED) diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 2221644bff47..6b1ef3acdf61 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -150,7 +150,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Set; @@ -2099,11 +2098,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { continue; } - mResolvedInstructionSets.add(archSubDir.getName()); - List<File> oatFiles = Arrays.asList(archSubDir.listFiles()); - if (!oatFiles.isEmpty()) { - mResolvedInheritedFiles.addAll(oatFiles); + File[] files = archSubDir.listFiles(); + if (files == null || files.length == 0) { + continue; } + + mResolvedInstructionSets.add(archSubDir.getName()); + mResolvedInheritedFiles.addAll(Arrays.asList(files)); } } } @@ -2117,7 +2118,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (!libDir.exists() || !libDir.isDirectory()) { continue; } - final List<File> libDirsToInherit = new LinkedList<>(); + final List<String> libDirsToInherit = new ArrayList<>(); + final List<File> libFilesToInherit = new ArrayList<>(); for (File archSubDir : libDir.listFiles()) { if (!archSubDir.isDirectory()) { continue; @@ -2129,14 +2131,24 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { Slog.e(TAG, "Skipping linking of native library directory!", e); // shouldn't be possible, but let's avoid inheriting these to be safe libDirsToInherit.clear(); + libFilesToInherit.clear(); break; } - if (!mResolvedNativeLibPaths.contains(relLibPath)) { - mResolvedNativeLibPaths.add(relLibPath); + + File[] files = archSubDir.listFiles(); + if (files == null || files.length == 0) { + continue; + } + + libDirsToInherit.add(relLibPath); + libFilesToInherit.addAll(Arrays.asList(files)); + } + for (String subDir : libDirsToInherit) { + if (!mResolvedNativeLibPaths.contains(subDir)) { + mResolvedNativeLibPaths.add(subDir); } - libDirsToInherit.addAll(Arrays.asList(archSubDir.listFiles())); } - mResolvedInheritedFiles.addAll(libDirsToInherit); + mResolvedInheritedFiles.addAll(libFilesToInherit); } } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 07078f26d0b4..5447bcb246e0 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -351,6 +351,7 @@ import com.android.server.pm.dex.DexManager; import com.android.server.pm.dex.DexoptOptions; import com.android.server.pm.dex.PackageDexUsage; import com.android.server.pm.dex.ViewCompiler; +import com.android.server.pm.parsing.PackageCacher; import com.android.server.pm.parsing.PackageInfoUtils; import com.android.server.pm.parsing.PackageParser2; import com.android.server.pm.parsing.library.PackageBackwardCompatibility; @@ -1346,13 +1347,6 @@ public class PackageManagerService extends IPackageManager.Stub int updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; boolean needUpdate = false; - if (DEBUG_DOMAIN_VERIFICATION) { - Slog.d(TAG, - "Updating IntentFilterVerificationInfo for package " + packageName - + " verificationId:" + verificationId - + " verified=" + verified); - } - // In a success case, we promote from undefined or ASK to ALWAYS. This // supports a flow where the app fails validation but then ships an updated // APK that passes, and therefore deserves to be in ALWAYS. @@ -17639,65 +17633,120 @@ public class PackageManagerService extends IPackageManager.Stub + " Activities needs verification ..."); int count = 0; - + boolean handlesWebUris = false; + ArraySet<String> domains = new ArraySet<>(); + final boolean previouslyVerified; + boolean hostSetExpanded = false; + boolean needToRunVerify = false; synchronized (mLock) { // If this is a new install and we see that we've already run verification for this // package, we have nothing to do: it means the state was restored from backup. - if (!replacing) { - IntentFilterVerificationInfo ivi = - mSettings.getIntentFilterVerificationLPr(packageName); - if (ivi != null) { - if (DEBUG_DOMAIN_VERIFICATION) { - Slog.i(TAG, "Package " + packageName+ " already verified: status=" - + ivi.getStatusString()); - } - return; + IntentFilterVerificationInfo ivi = + mSettings.getIntentFilterVerificationLPr(packageName); + previouslyVerified = (ivi != null); + if (!replacing && previouslyVerified) { + if (DEBUG_DOMAIN_VERIFICATION) { + Slog.i(TAG, "Package " + packageName + " already verified: status=" + + ivi.getStatusString()); } + return; } - // If any filters need to be verified, then all need to be. - boolean needToVerify = false; + if (DEBUG_DOMAIN_VERIFICATION) { + Slog.i(TAG, " Previous verified hosts: " + + (ivi == null ? "[none]" : ivi.getDomainsString())); + } + + // If any filters need to be verified, then all need to be. In addition, we need to + // know whether an updating app has any web navigation intent filters, to re- + // examine handling policy even if not re-verifying. + final boolean needsVerification = needsNetworkVerificationLPr(packageName); for (ParsedActivity a : activities) { for (ParsedIntentInfo filter : a.getIntents()) { - if (filter.needsVerification() - && needsNetworkVerificationLPr(a.getPackageName())) { + if (filter.handlesWebUris(true)) { + handlesWebUris = true; + } + if (needsVerification && filter.needsVerification()) { if (DEBUG_DOMAIN_VERIFICATION) { - Slog.d(TAG, - "Intent filter needs verification, so processing all filters"); + Slog.d(TAG, "autoVerify requested, processing all filters"); } - needToVerify = true; + needToRunVerify = true; + // It's safe to break out here because filter.needsVerification() + // can only be true if filter.handlesWebUris(true) returned true, so + // we've already noted that. break; } } } - if (needToVerify) { - final boolean needsVerification = needsNetworkVerificationLPr(packageName); + // Compare the new set of recognized hosts if the app is either requesting + // autoVerify or has previously used autoVerify but no longer does. + if (needToRunVerify || previouslyVerified) { final int verificationId = mIntentFilterVerificationToken++; for (ParsedActivity a : activities) { for (ParsedIntentInfo filter : a.getIntents()) { // Run verification against hosts mentioned in any web-nav intent filter, // even if the filter matches non-web schemes as well - if (needsVerification && filter.handlesWebUris(false)) { + if (filter.handlesWebUris(false /*onlyWebSchemes*/)) { if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG, "Verification needed for IntentFilter:" + filter.toString()); mIntentFilterVerifier.addOneIntentFilterVerification( verifierUid, userId, verificationId, filter, packageName); + domains.addAll(filter.getHostsList()); count++; } } } } + + if (DEBUG_DOMAIN_VERIFICATION) { + Slog.i(TAG, " Update published hosts: " + domains.toString()); + } + + // If we've previously verified this same host set (or a subset), we can trust that + // a current ALWAYS policy is still applicable. If this is the case, we're done. + // (If we aren't in ALWAYS, we want to reverify to allow for apps that had failing + // hosts in their intent filters, then pushed a new apk that removed them and now + // passes.) + // + // Cases: + // + still autoVerify (needToRunVerify): + // - preserve current state if all of: unexpanded, in always + // - otherwise rerun as usual (fall through) + // + no longer autoVerify (alreadyVerified && !needToRunVerify) + // - wipe verification history always + // - preserve current state if all of: unexpanded, in always + hostSetExpanded = !previouslyVerified + || (ivi != null && !ivi.getDomains().containsAll(domains)); + final int currentPolicy = + mSettings.getIntentFilterVerificationStatusLPr(packageName, userId); + final boolean keepCurState = !hostSetExpanded + && currentPolicy == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS; + + if (needToRunVerify && keepCurState) { + if (DEBUG_DOMAIN_VERIFICATION) { + Slog.i(TAG, "Host set not expanding + ALWAYS -> no need to reverify"); + } + ivi.setDomains(domains); + scheduleWriteSettingsLocked(); + return; + } else if (previouslyVerified && !needToRunVerify) { + // Prior autoVerify state but not requesting it now. Clear autoVerify history, + // and preserve the always policy iff the host set is not expanding. + clearIntentFilterVerificationsLPw(packageName, userId, !keepCurState); + return; + } } - if (count > 0) { + if (needToRunVerify && count > 0) { + // app requested autoVerify and has at least one matching intent filter if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG, "Starting " + count + " IntentFilter verification" + (count > 1 ? "s" : "") + " for userId:" + userId); mIntentFilterVerifier.startVerifications(userId); } else { if (DEBUG_DOMAIN_VERIFICATION) { - Slog.d(TAG, "No filters or not all autoVerify for " + packageName); + Slog.d(TAG, "No web filters or no new host policy for " + packageName); } } } @@ -18402,7 +18451,7 @@ public class PackageManagerService extends IPackageManager.Stub if ((flags & PackageManager.DELETE_KEEP_DATA) == 0) { final SparseBooleanArray changedUsers = new SparseBooleanArray(); synchronized (mLock) { - clearIntentFilterVerificationsLPw(deletedPs.name, UserHandle.USER_ALL); + clearIntentFilterVerificationsLPw(deletedPs.name, UserHandle.USER_ALL, true); clearDefaultBrowserIfNeeded(packageName); mSettings.mKeySetManagerService.removeAppKeySetDataLPw(packageName); removedAppId = mSettings.removePackageLPw(packageName); @@ -19495,13 +19544,14 @@ public class PackageManagerService extends IPackageManager.Stub final int packageCount = mPackages.size(); for (int i = 0; i < packageCount; i++) { AndroidPackage pkg = mPackages.valueAt(i); - clearIntentFilterVerificationsLPw(pkg.getPackageName(), userId); + clearIntentFilterVerificationsLPw(pkg.getPackageName(), userId, true); } } /** This method takes a specific user id as well as UserHandle.USER_ALL. */ @GuardedBy("mLock") - void clearIntentFilterVerificationsLPw(String packageName, int userId) { + void clearIntentFilterVerificationsLPw(String packageName, int userId, + boolean alsoResetStatus) { if (userId == UserHandle.USER_ALL) { if (mSettings.removeIntentFilterVerificationLPw(packageName, mUserManager.getUserIds())) { @@ -19510,7 +19560,8 @@ public class PackageManagerService extends IPackageManager.Stub } } } else { - if (mSettings.removeIntentFilterVerificationLPw(packageName, userId)) { + if (mSettings.removeIntentFilterVerificationLPw(packageName, userId, + alsoResetStatus)) { scheduleWritePackageRestrictionsLocked(userId); } } @@ -24182,6 +24233,25 @@ public class PackageManagerService extends IPackageManager.Stub } @Override + public void pruneCachedApksInApex(@NonNull List<PackageInfo> apexPackages) { + if (mCacheDir == null) { + return; + } + + final PackageCacher cacher = new PackageCacher(mCacheDir); + synchronized (mLock) { + for (int i = 0, size = apexPackages.size(); i < size; i++) { + final List<String> apkNames = + mApexManager.getApksInApex(apexPackages.get(i).packageName); + for (int j = 0, apksInApex = apkNames.size(); j < apksInApex; j++) { + final AndroidPackage pkg = getPackage(apkNames.get(j)); + cacher.cleanCachedResult(new File(pkg.getCodePath())); + } + } + } + } + + @Override public String getSetupWizardPackageName() { return mSetupWizardPackage; } diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 53c057a58a15..44a61d895be5 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -1282,7 +1282,8 @@ public final class Settings { return result; } - boolean removeIntentFilterVerificationLPw(String packageName, int userId) { + boolean removeIntentFilterVerificationLPw(String packageName, int userId, + boolean alsoResetStatus) { PackageSetting ps = mPackages.get(packageName); if (ps == null) { if (DEBUG_DOMAIN_VERIFICATION) { @@ -1290,14 +1291,17 @@ public final class Settings { } return false; } - ps.clearDomainVerificationStatusForUser(userId); + if (alsoResetStatus) { + ps.clearDomainVerificationStatusForUser(userId); + } + ps.setIntentFilterVerificationInfo(null); return true; } boolean removeIntentFilterVerificationLPw(String packageName, int[] userIds) { boolean result = false; for (int userId : userIds) { - result |= removeIntentFilterVerificationLPw(packageName, userId); + result |= removeIntentFilterVerificationLPw(packageName, userId, true); } return result; } diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index d1d98775c4b6..1c1e64d70bbd 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java @@ -1226,8 +1226,9 @@ public class StagingManager { // APEX checks. For single-package sessions, check if they contain an APEX. For // multi-package sessions, find all the child sessions that contain an APEX. if (hasApex) { + final List<PackageInfo> apexPackages; try { - final List<PackageInfo> apexPackages = submitSessionToApexService(session); + apexPackages = submitSessionToApexService(session); for (int i = 0, size = apexPackages.size(); i < size; i++) { validateApexSignature(apexPackages.get(i)); } @@ -1235,6 +1236,10 @@ public class StagingManager { session.setStagedSessionFailed(e.error, e.getMessage()); return; } + + final PackageManagerInternal packageManagerInternal = + LocalServices.getService(PackageManagerInternal.class); + packageManagerInternal.pruneCachedApksInApex(apexPackages); } notifyPreRebootVerification_Apex_Complete(session.sessionId); diff --git a/services/core/java/com/android/server/pm/parsing/PackageCacher.java b/services/core/java/com/android/server/pm/parsing/PackageCacher.java index e5e1b0b20955..99c6dd1f0312 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageCacher.java +++ b/services/core/java/com/android/server/pm/parsing/PackageCacher.java @@ -18,6 +18,7 @@ package com.android.server.pm.parsing; import android.annotation.NonNull; import android.content.pm.PackageParserCacheHelper; +import android.os.FileUtils; import android.os.Parcel; import android.system.ErrnoException; import android.system.Os; @@ -197,4 +198,18 @@ public class PackageCacher { Slog.w(TAG, "Error saving package cache.", e); } } + + /** + * Delete the cache files for the given {@code packageFile}. + */ + public void cleanCachedResult(@NonNull File packageFile) { + final String packageName = packageFile.getName(); + final File[] files = FileUtils.listFilesOrEmpty(mCacheDir, + (dir, name) -> name.startsWith(packageName)); + for (File file : files) { + if (!file.delete()) { + Slog.e(TAG, "Unable to clean cache file: " + file); + } + } + } } diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java index 059861b65e20..701197e690fc 100644 --- a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java +++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java @@ -214,7 +214,7 @@ public class BatterySaverPolicy extends ContentObserver { * required adjustments. */ @GuardedBy("mLock") - private Policy mEffectivePolicy = OFF_POLICY; + private Policy mEffectivePolicyRaw = OFF_POLICY; @IntDef(prefix = {"POLICY_LEVEL_"}, value = { POLICY_LEVEL_OFF, @@ -228,12 +228,8 @@ public class BatterySaverPolicy extends ContentObserver { static final int POLICY_LEVEL_ADAPTIVE = 1; static final int POLICY_LEVEL_FULL = 2; - /** - * Do not access directly; always use {@link #setPolicyLevel} - * and {@link #getPolicyLevelLocked} - */ @GuardedBy("mLock") - private int mPolicyLevelRaw = POLICY_LEVEL_OFF; + private int mPolicyLevel = POLICY_LEVEL_OFF; private final Context mContext; private final ContentResolver mContentResolver; @@ -338,7 +334,7 @@ public class BatterySaverPolicy extends ContentObserver { private void maybeNotifyListenersOfPolicyChange() { final BatterySaverPolicyListener[] listeners; synchronized (mLock) { - if (getPolicyLevelLocked() == POLICY_LEVEL_OFF) { + if (mPolicyLevel == POLICY_LEVEL_OFF) { // Current policy is OFF, so there's no change to notify listeners of. return; } @@ -428,14 +424,14 @@ public class BatterySaverPolicy extends ContentObserver { boolean changed = false; Policy newFullPolicy = Policy.fromSettings(setting, deviceSpecificSetting, DEFAULT_FULL_POLICY); - if (getPolicyLevelLocked() == POLICY_LEVEL_FULL && !mFullPolicy.equals(newFullPolicy)) { + if (mPolicyLevel == POLICY_LEVEL_FULL && !mFullPolicy.equals(newFullPolicy)) { changed = true; } mFullPolicy = newFullPolicy; mDefaultAdaptivePolicy = Policy.fromSettings(adaptiveSetting, adaptiveDeviceSpecificSetting, DEFAULT_ADAPTIVE_POLICY); - if (getPolicyLevelLocked() == POLICY_LEVEL_ADAPTIVE + if (mPolicyLevel == POLICY_LEVEL_ADAPTIVE && !mAdaptivePolicy.equals(mDefaultAdaptivePolicy)) { changed = true; } @@ -451,8 +447,9 @@ public class BatterySaverPolicy extends ContentObserver { @GuardedBy("mLock") private void updatePolicyDependenciesLocked() { final Policy rawPolicy = getCurrentRawPolicyLocked(); - final int locationMode; + + invalidatePowerSaveModeCaches(); if (mCarModeEnabled && rawPolicy.locationMode != PowerManager.LOCATION_MODE_NO_CHANGE && rawPolicy.locationMode != PowerManager.LOCATION_MODE_FOREGROUND_ONLY) { @@ -461,7 +458,8 @@ public class BatterySaverPolicy extends ContentObserver { } else { locationMode = rawPolicy.locationMode; } - mEffectivePolicy = new Policy( + + mEffectivePolicyRaw = new Policy( rawPolicy.adjustBrightnessFactor, rawPolicy.advertiseIsEnabled, rawPolicy.deferFullBackup, @@ -489,24 +487,24 @@ public class BatterySaverPolicy extends ContentObserver { final StringBuilder sb = new StringBuilder(); - if (mEffectivePolicy.forceAllAppsStandby) sb.append("A"); - if (mEffectivePolicy.forceBackgroundCheck) sb.append("B"); + if (mEffectivePolicyRaw.forceAllAppsStandby) sb.append("A"); + if (mEffectivePolicyRaw.forceBackgroundCheck) sb.append("B"); - if (mEffectivePolicy.disableVibration) sb.append("v"); - if (mEffectivePolicy.disableAnimation) sb.append("a"); - if (mEffectivePolicy.disableSoundTrigger) sb.append("s"); - if (mEffectivePolicy.deferFullBackup) sb.append("F"); - if (mEffectivePolicy.deferKeyValueBackup) sb.append("K"); - if (mEffectivePolicy.enableFirewall) sb.append("f"); - if (mEffectivePolicy.enableDataSaver) sb.append("d"); - if (mEffectivePolicy.enableAdjustBrightness) sb.append("b"); + if (mEffectivePolicyRaw.disableVibration) sb.append("v"); + if (mEffectivePolicyRaw.disableAnimation) sb.append("a"); + if (mEffectivePolicyRaw.disableSoundTrigger) sb.append("s"); + if (mEffectivePolicyRaw.deferFullBackup) sb.append("F"); + if (mEffectivePolicyRaw.deferKeyValueBackup) sb.append("K"); + if (mEffectivePolicyRaw.enableFirewall) sb.append("f"); + if (mEffectivePolicyRaw.enableDataSaver) sb.append("d"); + if (mEffectivePolicyRaw.enableAdjustBrightness) sb.append("b"); - if (mEffectivePolicy.disableLaunchBoost) sb.append("l"); - if (mEffectivePolicy.disableOptionalSensors) sb.append("S"); - if (mEffectivePolicy.disableAod) sb.append("o"); - if (mEffectivePolicy.enableQuickDoze) sb.append("q"); + if (mEffectivePolicyRaw.disableLaunchBoost) sb.append("l"); + if (mEffectivePolicyRaw.disableOptionalSensors) sb.append("S"); + if (mEffectivePolicyRaw.disableAod) sb.append("o"); + if (mEffectivePolicyRaw.enableQuickDoze) sb.append("q"); - sb.append(mEffectivePolicy.locationMode); + sb.append(mEffectivePolicyRaw.locationMode); mEventLogKeys = sb.toString(); } @@ -969,14 +967,14 @@ public class BatterySaverPolicy extends ContentObserver { */ boolean setPolicyLevel(@PolicyLevel int level) { synchronized (mLock) { - if (getPolicyLevelLocked() == level) { + if (mPolicyLevel == level) { return false; } switch (level) { case POLICY_LEVEL_FULL: case POLICY_LEVEL_ADAPTIVE: case POLICY_LEVEL_OFF: - setPolicyLevelLocked(level); + mPolicyLevel = level; break; default: Slog.wtf(TAG, "setPolicyLevel invalid level given: " + level); @@ -998,7 +996,7 @@ public class BatterySaverPolicy extends ContentObserver { } mAdaptivePolicy = p; - if (getPolicyLevelLocked() == POLICY_LEVEL_ADAPTIVE) { + if (mPolicyLevel == POLICY_LEVEL_ADAPTIVE) { updatePolicyDependenciesLocked(); return true; } @@ -1011,11 +1009,11 @@ public class BatterySaverPolicy extends ContentObserver { } private Policy getCurrentPolicyLocked() { - return mEffectivePolicy; + return mEffectivePolicyRaw; } private Policy getCurrentRawPolicyLocked() { - switch (getPolicyLevelLocked()) { + switch (mPolicyLevel) { case POLICY_LEVEL_FULL: return mFullPolicy; case POLICY_LEVEL_ADAPTIVE: @@ -1077,12 +1075,12 @@ public class BatterySaverPolicy extends ContentObserver { pw.println(" mAccessibilityEnabled=" + mAccessibilityEnabled); pw.println(" mCarModeEnabled=" + mCarModeEnabled); - pw.println(" mPolicyLevel=" + getPolicyLevelLocked()); + pw.println(" mPolicyLevel=" + mPolicyLevel); dumpPolicyLocked(pw, " ", "full", mFullPolicy); dumpPolicyLocked(pw, " ", "default adaptive", mDefaultAdaptivePolicy); dumpPolicyLocked(pw, " ", "current adaptive", mAdaptivePolicy); - dumpPolicyLocked(pw, " ", "effective", mEffectivePolicy); + dumpPolicyLocked(pw, " ", "effective", mEffectivePolicyRaw); } } @@ -1170,20 +1168,4 @@ public class BatterySaverPolicy extends ContentObserver { } } } - - /** Non-blocking getter exists as a reminder not to modify cached fields directly */ - @GuardedBy("mLock") - private int getPolicyLevelLocked() { - return mPolicyLevelRaw; - } - - @GuardedBy("mLock") - private void setPolicyLevelLocked(int level) { - if (mPolicyLevelRaw == level) { - return; - } - // Under lock, invalidate before set ensures caches won't return stale values. - invalidatePowerSaveModeCaches(); - mPolicyLevelRaw = level; - } } diff --git a/services/core/java/com/android/server/tv/TvRemoteServiceInput.java b/services/core/java/com/android/server/tv/TvRemoteServiceInput.java index 8fe6da5e8dbe..390340a13e51 100644 --- a/services/core/java/com/android/server/tv/TvRemoteServiceInput.java +++ b/services/core/java/com/android/server/tv/TvRemoteServiceInput.java @@ -88,6 +88,47 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { } @Override + public void openGamepadBridge(IBinder token, String name) throws RemoteException { + if (DEBUG) { + Slog.d(TAG, String.format("openGamepadBridge(), token: %s, name: %s", token, name)); + } + + synchronized (mLock) { + if (mBridgeMap.containsKey(token)) { + if (DEBUG) { + Slog.d(TAG, "InputBridge already exists"); + } + } else { + final long idToken = Binder.clearCallingIdentity(); + try { + mBridgeMap.put(token, UinputBridge.openGamepad(token, name)); + token.linkToDeath(new IBinder.DeathRecipient() { + @Override + public void binderDied() { + closeInputBridge(token); + } + }, 0); + } catch (IOException e) { + Slog.e(TAG, "Cannot create device for " + name); + return; + } catch (RemoteException e) { + Slog.e(TAG, "Token is already dead"); + closeInputBridge(token); + return; + } finally { + Binder.restoreCallingIdentity(idToken); + } + } + } + + try { + mProvider.onInputBridgeConnected(token); + } catch (RemoteException e) { + Slog.e(TAG, "Failed remote call to onInputBridgeConnected"); + } + } + + @Override public void closeInputBridge(IBinder token) { if (DEBUG) { Slog.d(TAG, "closeInputBridge(), token: " + token); @@ -96,6 +137,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.remove(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -117,6 +159,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.get(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -145,6 +188,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.get(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -166,6 +210,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.get(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -188,6 +233,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.get(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -209,6 +255,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.get(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -230,6 +277,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.get(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -241,4 +289,67 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { } } } + + @Override + public void sendGamepadKeyUp(IBinder token, int keyIndex) { + if (DEBUG_KEYS) { + Slog.d(TAG, String.format("sendGamepadKeyUp(), token: %s", token)); + } + synchronized (mLock) { + UinputBridge inputBridge = mBridgeMap.get(token); + if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); + return; + } + + final long idToken = Binder.clearCallingIdentity(); + try { + inputBridge.sendGamepadKey(token, keyIndex, false); + } finally { + Binder.restoreCallingIdentity(idToken); + } + } + } + + @Override + public void sendGamepadKeyDown(IBinder token, int keyCode) { + if (DEBUG_KEYS) { + Slog.d(TAG, String.format("sendGamepadKeyDown(), token: %s", token)); + } + synchronized (mLock) { + UinputBridge inputBridge = mBridgeMap.get(token); + if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); + return; + } + + final long idToken = Binder.clearCallingIdentity(); + try { + inputBridge.sendGamepadKey(token, keyCode, true); + } finally { + Binder.restoreCallingIdentity(idToken); + } + } + } + + @Override + public void sendGamepadAxisValue(IBinder token, int axis, float value) { + if (DEBUG_KEYS) { + Slog.d(TAG, String.format("sendGamepadAxisValue(), token: %s", token)); + } + synchronized (mLock) { + UinputBridge inputBridge = mBridgeMap.get(token); + if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); + return; + } + + final long idToken = Binder.clearCallingIdentity(); + try { + inputBridge.sendGamepadAxisValue(token, axis, value); + } finally { + Binder.restoreCallingIdentity(idToken); + } + } + } } diff --git a/services/core/java/com/android/server/tv/UinputBridge.java b/services/core/java/com/android/server/tv/UinputBridge.java index a2fe5fcde8c2..1dc201d4ee6b 100644 --- a/services/core/java/com/android/server/tv/UinputBridge.java +++ b/services/core/java/com/android/server/tv/UinputBridge.java @@ -42,21 +42,27 @@ public final class UinputBridge { /** Opens a gamepad - will support gamepad key and axis sending */ private static native long nativeGamepadOpen(String name, String uniqueId); - /** Marks the specified key up/down for a gamepad */ - private static native void nativeSendGamepadKey(long ptr, int keyIndex, boolean down); + /** + * Marks the specified key up/down for a gamepad. + * + * @param keyCode - a code like BUTTON_MODE, BUTTON_A, BUTTON_B, ... + */ + private static native void nativeSendGamepadKey(long ptr, int keyCode, boolean down); /** - * Gamepads pre-define the following axes: - * - Left joystick X, axis == ABS_X == 0, range [0, 254] - * - Left joystick Y, axis == ABS_Y == 1, range [0, 254] - * - Right joystick X, axis == ABS_RX == 3, range [0, 254] - * - Right joystick Y, axis == ABS_RY == 4, range [0, 254] - * - Left trigger, axis == ABS_Z == 2, range [0, 254] - * - Right trigger, axis == ABS_RZ == 5, range [0, 254] - * - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1] - * - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1] + * Send an axis value. + * + * Available axes are: + * <li> Left joystick: AXIS_X, AXIS_Y + * <li> Right joystick: AXIS_Z, AXIS_RZ + * <li> Analog triggers: AXIS_LTRIGGER, AXIS_RTRIGGER + * <li> DPad: AXIS_HAT_X, AXIS_HAT_Y + * + * @param axis is a MotionEvent.AXIS_* value. + * @param value is a value between -1 and 1 (inclusive) + * */ - private static native void nativeSendGamepadAxisValue(long ptr, int axis, int value); + private static native void nativeSendGamepadAxisValue(long ptr, int axis, float value); public UinputBridge(IBinder token, String name, int width, int height, int maxPointers) throws IOException { @@ -163,26 +169,19 @@ public final class UinputBridge { * @param keyIndex - the index of the w3-spec key * @param down - is the key pressed ? */ - public void sendGamepadKey(IBinder token, int keyIndex, boolean down) { + public void sendGamepadKey(IBinder token, int keyCode, boolean down) { if (isTokenValid(token)) { - nativeSendGamepadKey(mPtr, keyIndex, down); + nativeSendGamepadKey(mPtr, keyCode, down); } } - /** Send a gamepad axis value. - * - Left joystick X, axis == ABS_X == 0, range [0, 254] - * - Left joystick Y, axis == ABS_Y == 1, range [0, 254] - * - Right joystick X, axis == ABS_RX == 3, range [0, 254] - * - Right joystick Y, axis == ABS_RY == 4, range [0, 254] - * - Left trigger, axis == ABS_Z == 2, range [0, 254] - * - Right trigger, axis == ABS_RZ == 5, range [0, 254] - * - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1] - * - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1] + /** + * Send a gamepad axis value. * - * @param axis is the axis index - * @param value is the value to set for that axis + * @param axis is the axis code (MotionEvent.AXIS_*) + * @param value is the value to set for that axis in [-1, 1] */ - public void sendGamepadAxisValue(IBinder token, int axis, int value) { + public void sendGamepadAxisValue(IBinder token, int axis, float value) { if (isTokenValid(token)) { nativeSendGamepadAxisValue(mPtr, axis, value); } diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 3f9f95cf8370..abccf99579b7 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -1245,6 +1245,16 @@ final class AccessibilityController { } } + for (int i = dc.mShellRoots.size() - 1; i >= 0; --i) { + final WindowInfo info = dc.mShellRoots.valueAt(i).getWindowInfo(); + if (info == null) { + continue; + } + info.layer = addedWindows.size(); + windows.add(info); + addedWindows.add(info.token); + } + // Remove child/parent references to windows that were not added. final int windowCount = windows.size(); for (int i = 0; i < windowCount; i++) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index e76eda06d2d3..2648c86d3c6a 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -4712,7 +4712,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ private boolean shouldBeResumed(ActivityRecord activeActivity) { return shouldMakeActive(activeActivity) && isFocusable() - && getRootTask().getVisibility(activeActivity) == STACK_VISIBILITY_VISIBLE + && getTask().getVisibility(activeActivity) == STACK_VISIBILITY_VISIBLE && canResumeByCompat(); } diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java index 0f574963eb34..ff43e77a5c47 100644 --- a/services/core/java/com/android/server/wm/ActivityStack.java +++ b/services/core/java/com/android/server/wm/ActivityStack.java @@ -18,11 +18,8 @@ package com.android.server.wm; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.PINNED_WINDOWING_MODE_ELEVATION_IN_DIP; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; @@ -142,13 +139,11 @@ import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; import android.service.voice.IVoiceInteractionSession; -import android.util.DisplayMetrics; import android.util.Log; import android.util.Slog; import android.util.proto.ProtoOutputStream; import android.view.Display; import android.view.DisplayInfo; -import android.view.SurfaceControl; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -157,7 +152,6 @@ import com.android.internal.os.logging.MetricsLoggerWrapper; import com.android.internal.util.function.pooled.PooledConsumer; import com.android.internal.util.function.pooled.PooledFunction; import com.android.internal.util.function.pooled.PooledLambda; -import com.android.internal.util.function.pooled.PooledPredicate; import com.android.server.Watchdog; import com.android.server.am.ActivityManagerService; import com.android.server.am.ActivityManagerService.ItemMatcher; @@ -276,11 +270,6 @@ class ActivityStack extends Task { Rect mPreAnimationBounds = new Rect(); - /** - * For {@link #prepareSurfaces}. - */ - private final Point mLastSurfaceSize = new Point(); - private final AnimatingActivityRegistry mAnimatingActivityRegistry = new AnimatingActivityRegistry(); @@ -606,10 +595,6 @@ class ActivityStack extends Task { super.onConfigurationChanged(newParentConfig); - // Only need to update surface size here since the super method will handle updating - // surface position. - updateSurfaceSize(getPendingTransaction()); - final TaskDisplayArea taskDisplayArea = getDisplayArea(); if (taskDisplayArea == null) { return; @@ -3262,61 +3247,14 @@ class ActivityStack extends Task { scheduleAnimation(); } - /** - * Calculate an amount by which to expand the stack bounds in each direction. - * Used to make room for shadows in the pinned windowing mode. - */ - int getStackOutset() { - // If we are drawing shadows on the task then don't outset the stack. - if (mWmService.mRenderShadowsInCompositor) { - return 0; - } - DisplayContent displayContent = getDisplayContent(); - if (inPinnedWindowingMode() && displayContent != null) { - final DisplayMetrics displayMetrics = displayContent.getDisplayMetrics(); - - // We multiply by two to match the client logic for converting view elevation - // to insets, as in {@link WindowManager.LayoutParams#setSurfaceInsets} - return (int) Math.ceil( - mWmService.dipToPixel(PINNED_WINDOWING_MODE_ELEVATION_IN_DIP, displayMetrics) - * 2); - } - return 0; - } - @Override void getRelativePosition(Point outPos) { super.getRelativePosition(outPos); - final int outset = getStackOutset(); + final int outset = getTaskOutset(); outPos.x -= outset; outPos.y -= outset; } - private void updateSurfaceSize(SurfaceControl.Transaction transaction) { - if (mSurfaceControl == null) { - return; - } - - final Rect stackBounds = getBounds(); - int width = stackBounds.width(); - int height = stackBounds.height(); - - final int outset = getStackOutset(); - width += 2 * outset; - height += 2 * outset; - - if (width == mLastSurfaceSize.x && height == mLastSurfaceSize.y) { - return; - } - transaction.setWindowCrop(mSurfaceControl, width, height); - mLastSurfaceSize.set(width, height); - } - - @VisibleForTesting - Point getLastSurfaceSize() { - return mLastSurfaceSize; - } - @Override void onDisplayChanged(DisplayContent dc) { super.onDisplayChanged(dc); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index a47cdc66fbd8..e26163247020 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -26,7 +26,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; @@ -574,7 +573,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo /** Corner radius that windows should have in order to match the display. */ private final float mWindowCornerRadius; - private final SparseArray<ShellRoot> mShellRoots = new SparseArray<>(); + final SparseArray<ShellRoot> mShellRoots = new SparseArray<>(); RemoteInsetsControlTarget mRemoteInsetsControlTarget = null; private final IBinder.DeathRecipient mRemoteInsetsDeath = () -> { @@ -5450,6 +5449,46 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo return mWmService.mDisplayManagerInternal.getDisplayPosition(getDisplayId()); } + /** + * Locates the appropriate target window for scroll capture. The search progresses top to + * bottom. + * If {@code searchBehind} is non-null, the search will only consider windows behind this one. + * If a valid taskId is specified, the target window must belong to the given task. + * + * @param searchBehind a window used to filter the search to windows behind it, or null to begin + * the search at the top window of the display + * @param taskId specifies the id of a task the result must belong to or + * {@link android.app.ActivityTaskManager#INVALID_TASK_ID INVALID_TASK_ID} + * to match any window + * @return the located window or null if none could be found matching criteria + */ + @Nullable + WindowState findScrollCaptureTargetWindow(@Nullable WindowState searchBehind, int taskId) { + return getWindow(new Predicate<WindowState>() { + boolean behindTopWindow = (searchBehind == null); // optional filter + @Override + public boolean test(WindowState nextWindow) { + // Skip through all windows until we pass topWindow (if specified) + if (!behindTopWindow) { + if (nextWindow == searchBehind) { + behindTopWindow = true; + } + return false; /* continue */ + } + if (taskId != INVALID_TASK_ID) { + Task task = nextWindow.getTask(); + if (task == null || !task.isTaskId(taskId)) { + return false; /* continue */ + } + } + if (!nextWindow.canReceiveKeys()) { + return false; /* continue */ + } + return true; /* stop */ + } + }); + } + class RemoteInsetsControlTarget implements InsetsControlTarget { private final IDisplayWindowInsetsController mRemoteInsetsController; diff --git a/services/core/java/com/android/server/wm/DockedStackDividerController.java b/services/core/java/com/android/server/wm/DockedStackDividerController.java index 20738ed29470..803bec8941a8 100644 --- a/services/core/java/com/android/server/wm/DockedStackDividerController.java +++ b/services/core/java/com/android/server/wm/DockedStackDividerController.java @@ -45,6 +45,11 @@ public class DockedStackDividerController { void setTouchRegion(Rect touchRegion) { mTouchRegion.set(touchRegion); + // We need to report touchable region changes to accessibility. + if (mDisplayContent.mWmService.mAccessibilityController != null) { + mDisplayContent.mWmService.mAccessibilityController.onSomeWindowResizedOrMovedLocked( + mDisplayContent.getDisplayId()); + } } void getTouchRegion(Rect outRegion) { diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index 8b34b9b8dd8f..656dca531a22 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -487,7 +487,8 @@ final class InputMonitor { || w.cantReceiveTouchInput()) { if (w.mWinAnimator.hasSurface()) { mInputTransaction.setInputWindowInfo( - w.mWinAnimator.mSurfaceController.mSurfaceControl, mInvalidInputWindow); + w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), + mInvalidInputWindow); } // Skip this window because it cannot possibly receive input. return; @@ -560,7 +561,8 @@ final class InputMonitor { if (w.mWinAnimator.hasSurface()) { mInputTransaction.setInputWindowInfo( - w.mWinAnimator.mSurfaceController.mSurfaceControl, inputWindowHandle); + w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), + inputWindowHandle); } } } diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java index 5f33ea170923..9d44cad70281 100644 --- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java +++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java @@ -597,8 +597,8 @@ class ScreenRotationAnimation { return startAnimation(initializeBuilder() .setSurfaceControl(mScreenshotLayer) .setAnimationLeashParent(mDisplayContent.getOverlayLayer()) - .setWidth(mWidth) - .setHeight(mHeight) + .setWidth(mDisplayContent.getSurfaceWidth()) + .setHeight(mDisplayContent.getSurfaceHeight()) .build(), createWindowAnimationSpec(mRotateAlphaAnimation), this::onAnimationEnd); diff --git a/services/core/java/com/android/server/wm/ShellRoot.java b/services/core/java/com/android/server/wm/ShellRoot.java index 701feff8c6be..0b1760dc5a1c 100644 --- a/services/core/java/com/android/server/wm/ShellRoot.java +++ b/services/core/java/com/android/server/wm/ShellRoot.java @@ -23,12 +23,14 @@ import static com.android.server.wm.WindowManagerService.MAX_ANIMATION_DURATION; import android.annotation.NonNull; import android.graphics.Point; +import android.graphics.Rect; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; import android.view.DisplayInfo; import android.view.IWindow; import android.view.SurfaceControl; +import android.view.WindowInfo; import android.view.animation.Animation; /** @@ -102,5 +104,27 @@ public class ShellRoot { mToken.startAnimation(mToken.getPendingTransaction(), adapter, false /* hidden */, ANIMATION_TYPE_WINDOW_ANIMATION, null /* animationFinishedCallback */); } + + WindowInfo getWindowInfo() { + if (mToken.windowType != TYPE_DOCK_DIVIDER) { + return null; + } + if (!mDisplayContent.getDefaultTaskDisplayArea().isSplitScreenModeActivated()) { + return null; + } + WindowInfo windowInfo = WindowInfo.obtain(); + windowInfo.displayId = mToken.getDisplayArea().getDisplayContent().mDisplayId; + windowInfo.type = mToken.windowType; + windowInfo.layer = mToken.getWindowLayerFromType(); + windowInfo.token = mClient.asBinder(); + windowInfo.title = "Splitscreen Divider"; + windowInfo.focused = false; + windowInfo.inPictureInPicture = false; + windowInfo.hasFlagWatchOutsideTouch = false; + final Rect regionRect = new Rect(); + mDisplayContent.getDockedDividerController().getTouchRegion(regionRect); + windowInfo.regionInScreen.set(regionRect); + return windowInfo; + } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 66ca0ac85143..df5cfee6c01c 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -120,6 +120,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.graphics.Point; import android.graphics.Rect; import android.os.Debug; import android.os.IBinder; @@ -213,7 +214,6 @@ class Task extends WindowContainer<WindowContainer> { static final int INVALID_MIN_SIZE = -1; private float mShadowRadius = 0; - private final Rect mLastSurfaceCrop = new Rect(); /** * The modes to control how the stack is moved to the front when calling {@link Task#reparent}. @@ -397,6 +397,7 @@ class Task extends WindowContainer<WindowContainer> { private Dimmer mDimmer = new Dimmer(this); private final Rect mTmpDimBoundsRect = new Rect(); + private final Point mLastSurfaceSize = new Point(); /** @see #setCanAffectSystemUiFlags */ private boolean mCanAffectSystemUiFlags = true; @@ -1943,6 +1944,10 @@ class Task extends WindowContainer<WindowContainer> { mTmpPrevBounds.set(getBounds()); final boolean wasInMultiWindowMode = inMultiWindowMode(); super.onConfigurationChanged(newParentConfig); + // Only need to update surface size here since the super method will handle updating + // surface position. + updateSurfaceSize(getPendingTransaction()); + if (wasInMultiWindowMode != inMultiWindowMode()) { mStackSupervisor.scheduleUpdateMultiWindowMode(this); } @@ -1995,6 +2000,57 @@ class Task extends WindowContainer<WindowContainer> { return (prevWinMode == WINDOWING_MODE_FREEFORM) != (newWinMode == WINDOWING_MODE_FREEFORM); } + void updateSurfaceSize(SurfaceControl.Transaction transaction) { + if (mSurfaceControl == null || mCreatedByOrganizer) { + return; + } + + // Apply crop to root tasks only and clear the crops of the descendant tasks. + int width = 0; + int height = 0; + if (isRootTask()) { + final Rect taskBounds = getBounds(); + width = taskBounds.width(); + height = taskBounds.height(); + + final int outset = getTaskOutset(); + width += 2 * outset; + height += 2 * outset; + } + if (width == mLastSurfaceSize.x && height == mLastSurfaceSize.y) { + return; + } + transaction.setWindowCrop(mSurfaceControl, width, height); + mLastSurfaceSize.set(width, height); + } + + /** + * Calculate an amount by which to expand the task bounds in each direction. + * Used to make room for shadows in the pinned windowing mode. + */ + int getTaskOutset() { + // If we are drawing shadows on the task then don't outset the stack. + if (mWmService.mRenderShadowsInCompositor) { + return 0; + } + DisplayContent displayContent = getDisplayContent(); + if (inPinnedWindowingMode() && displayContent != null) { + final DisplayMetrics displayMetrics = displayContent.getDisplayMetrics(); + + // We multiply by two to match the client logic for converting view elevation + // to insets, as in {@link WindowManager.LayoutParams#setSurfaceInsets} + return (int) Math.ceil( + mWmService.dipToPixel(PINNED_WINDOWING_MODE_ELEVATION_IN_DIP, displayMetrics) + * 2); + } + return 0; + } + + @VisibleForTesting + Point getLastSurfaceSize() { + return mLastSurfaceSize; + } + @VisibleForTesting boolean isInChangeTransition() { return mSurfaceFreezer.hasLeash() || AppTransition.isChangeTransit(mTransit); @@ -2225,14 +2281,16 @@ class Task extends WindowContainer<WindowContainer> { } density *= DisplayMetrics.DENSITY_DEFAULT_SCALE; + // If bounds have been overridden at this level, restrict config resources to these bounds + // rather than the parent because the overridden bounds can be larger than the parent. + boolean hasOverrideBounds = false; + final Rect resolvedBounds = inOutConfig.windowConfiguration.getBounds(); - if (resolvedBounds == null) { - mTmpFullBounds.setEmpty(); + if (resolvedBounds == null || resolvedBounds.isEmpty()) { + mTmpFullBounds.set(parentConfig.windowConfiguration.getBounds()); } else { mTmpFullBounds.set(resolvedBounds); - } - if (mTmpFullBounds.isEmpty()) { - mTmpFullBounds.set(parentConfig.windowConfiguration.getBounds()); + hasOverrideBounds = true; } Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds(); @@ -2244,7 +2302,16 @@ class Task extends WindowContainer<WindowContainer> { // the out bounds doesn't need to be restricted by the parent. final boolean insideParentBounds = compatInsets == null; if (insideParentBounds && windowingMode != WINDOWING_MODE_FREEFORM) { - final Rect parentAppBounds = parentConfig.windowConfiguration.getAppBounds(); + Rect parentAppBounds; + if (hasOverrideBounds) { + // Since we overrode the bounds, restrict appBounds to display non-decor rather + // than parent. Otherwise, it won't match the overridden bounds. + final TaskDisplayArea displayArea = getDisplayArea(); + parentAppBounds = displayArea != null + ? displayArea.getConfiguration().windowConfiguration.getAppBounds() : null; + } else { + parentAppBounds = parentConfig.windowConfiguration.getAppBounds(); + } if (parentAppBounds != null && !parentAppBounds.isEmpty()) { outAppBounds.intersect(parentAppBounds); } @@ -2291,13 +2358,13 @@ class Task extends WindowContainer<WindowContainer> { if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) { final int overrideScreenWidthDp = (int) (mTmpStableBounds.width() / density); - inOutConfig.screenWidthDp = insideParentBounds + inOutConfig.screenWidthDp = (insideParentBounds && !hasOverrideBounds) ? Math.min(overrideScreenWidthDp, parentConfig.screenWidthDp) : overrideScreenWidthDp; } if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) { final int overrideScreenHeightDp = (int) (mTmpStableBounds.height() / density); - inOutConfig.screenHeightDp = insideParentBounds + inOutConfig.screenHeightDp = (insideParentBounds && !hasOverrideBounds) ? Math.min(overrideScreenHeightDp, parentConfig.screenHeightDp) : overrideScreenHeightDp; } @@ -2344,27 +2411,27 @@ class Task extends WindowContainer<WindowContainer> { mTmpBounds.set(getResolvedOverrideConfiguration().windowConfiguration.getBounds()); super.resolveOverrideConfiguration(newParentConfig); - // Resolve override windowing mode to fullscreen for home task (even on freeform - // display), or split-screen-secondary if in split-screen mode. int windowingMode = getResolvedOverrideConfiguration().windowConfiguration.getWindowingMode(); + + // Resolve override windowing mode to fullscreen for home task (even on freeform + // display), or split-screen if in split-screen mode. if (getActivityType() == ACTIVITY_TYPE_HOME && windowingMode == WINDOWING_MODE_UNDEFINED) { final int parentWindowingMode = newParentConfig.windowConfiguration.getWindowingMode(); - windowingMode = parentWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY - ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY - : WINDOWING_MODE_FULLSCREEN; + windowingMode = WindowConfiguration.isSplitScreenWindowingMode(parentWindowingMode) + ? parentWindowingMode : WINDOWING_MODE_FULLSCREEN; getResolvedOverrideConfiguration().windowConfiguration.setWindowingMode(windowingMode); } - if (!isLeafTask()) { - // Compute configuration overrides for tasks that created by organizer, so that - // organizer can get the correct configuration from those tasks. - if (mCreatedByOrganizer) { - computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig); - } - return; + if (isLeafTask()) { + resolveLeafOnlyOverrideConfigs(newParentConfig); } + computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig); + } + void resolveLeafOnlyOverrideConfigs(Configuration newParentConfig) { + int windowingMode = + getResolvedOverrideConfiguration().windowConfiguration.getWindowingMode(); if (windowingMode == WINDOWING_MODE_UNDEFINED) { windowingMode = newParentConfig.windowConfiguration.getWindowingMode(); } @@ -2404,7 +2471,6 @@ class Task extends WindowContainer<WindowContainer> { outOverrideBounds.offset(0, offsetTop); } } - computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig); } /** @@ -2803,28 +2869,6 @@ class Task extends WindowContainer<WindowContainer> { return boundsChange; } - private void updateSurfaceCrop() { - // Only update the crop if we are drawing shadows on the task. - if (mSurfaceControl == null || !mWmService.mRenderShadowsInCompositor || !isRootTask()) { - return; - } - - if (inSplitScreenWindowingMode()) { - // inherit crop from parent - mTmpRect.setEmpty(); - } else { - getBounds(mTmpRect); - } - - mTmpRect.offsetTo(0, 0); - if (mLastSurfaceCrop.equals(mTmpRect)) { - return; - } - - getPendingTransaction().setWindowCrop(mSurfaceControl, mTmpRect); - mLastSurfaceCrop.set(mTmpRect); - } - @Override public boolean onDescendantOrientationChanged(IBinder freezeDisplayToken, ConfigurationContainer requestingContainer) { @@ -3453,7 +3497,6 @@ class Task extends WindowContainer<WindowContainer> { mTmpDimBoundsRect.offsetTo(0, 0); } - updateSurfaceCrop(); updateShadowsRadius(isFocused(), getPendingTransaction()); if (mDimmer.updateDims(getPendingTransaction(), mTmpDimBoundsRect)) { diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index cb9b332a9f35..25791c762e42 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -267,16 +267,14 @@ final class TaskDisplayArea extends DisplayArea<ActivityStack> { @Override void addChild(ActivityStack stack, int position) { + if (DEBUG_STACK) Slog.d(TAG_WM, "Set stack=" + stack + " on taskDisplayArea=" + this); addStackReferenceIfNeeded(stack); position = findPositionForStack(position, stack, true /* adding */); super.addChild(stack, position); mAtmService.updateSleepIfNeededLocked(); - // The reparenting case is handled in WindowContainer. - if (!stack.mReparenting) { - mDisplayContent.setLayoutNeeded(); - } + positionStackAt(stack, position); } @Override @@ -638,12 +636,6 @@ final class TaskDisplayArea extends DisplayArea<ActivityStack> { } } - void addStack(ActivityStack stack, int position) { - if (DEBUG_STACK) Slog.d(TAG_WM, "Set stack=" + stack + " on taskDisplayArea=" + this); - addChild(stack, position); - positionStackAt(stack, position); - } - void onStackRemoved(ActivityStack stack) { if (ActivityTaskManagerDebugConfig.DEBUG_STACK) { Slog.v(TAG_STACK, "removeStack: detaching " + stack + " from displayId=" @@ -787,7 +779,7 @@ final class TaskDisplayArea extends DisplayArea<ActivityStack> { } } else if (stack.getDisplayArea() != this || !stack.isRootTask()) { if (stack.getParent() == null) { - addStack(stack, position); + addChild(stack, position); } else { stack.reparent(this, onTop); } @@ -943,7 +935,7 @@ final class TaskDisplayArea extends DisplayArea<ActivityStack> { positionStackAtTop((ActivityStack) launchRootTask, false /* includingParents */); } } else { - addStack(stack, onTop ? POSITION_TOP : POSITION_BOTTOM); + addChild(stack, onTop ? POSITION_TOP : POSITION_BOTTOM); stack.setWindowingMode(windowingMode, true /* creating */); } return stack; diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 899ab247077a..f3e2992d5913 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -662,9 +662,11 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // position that takes into account the removed child (if the index of the // child < position, then the position should be adjusted). We should consider // doing this adjustment here and remove any adjustments in the callers. - mChildren.remove(child); - mChildren.add(position, child); - onChildPositionChanged(child); + if (mChildren.indexOf(child) != position) { + mChildren.remove(child); + mChildren.add(position, child); + onChildPositionChanged(child); + } } } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index f55a1b3f6ab3..a5014145aa60 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -219,6 +219,7 @@ import android.view.IOnKeyguardExitResult; import android.view.IPinnedStackListener; import android.view.IRecentsAnimationRunner; import android.view.IRotationWatcher; +import android.view.IScrollCaptureController; import android.view.ISystemGestureExclusionListener; import android.view.IWallpaperVisibilityListener; import android.view.IWindow; @@ -6837,6 +6838,58 @@ public class WindowManagerService extends IWindowManager.Stub } } + /** + * Forwards a scroll capture request to the appropriate window, if available. + * + * @param displayId the display for the request + * @param behindClient token for a window, used to filter the search to windows behind it + * @param taskId specifies the id of a task the result must belong to or -1 to ignore task ids + * @param controller the controller to receive results; a call to either + * {@link IScrollCaptureController#onClientConnected} or + * {@link IScrollCaptureController#onClientUnavailable}. + */ + public void requestScrollCapture(int displayId, @Nullable IBinder behindClient, int taskId, + IScrollCaptureController controller) { + if (!checkCallingPermission(READ_FRAME_BUFFER, "requestScrollCapture()")) { + throw new SecurityException("Requires READ_FRAME_BUFFER permission"); + } + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + DisplayContent dc = mRoot.getDisplayContent(displayId); + if (dc == null) { + ProtoLog.e(WM_ERROR, + "Invalid displayId for requestScrollCapture: %d", displayId); + controller.onClientUnavailable(); + return; + } + WindowState topWindow = null; + if (behindClient != null) { + topWindow = windowForClientLocked(null, behindClient, /* throwOnError*/ true); + } + WindowState targetWindow = dc.findScrollCaptureTargetWindow(topWindow, taskId); + if (targetWindow == null) { + controller.onClientUnavailable(); + return; + } + // Forward to the window for handling. + try { + targetWindow.mClient.requestScrollCapture(controller); + } catch (RemoteException e) { + ProtoLog.w(WM_ERROR, + "requestScrollCapture: caught exception dispatching to window." + + "token=%s", targetWindow.mClient.asBinder()); + controller.onClientUnavailable(); + } + } + } catch (RemoteException e) { + ProtoLog.w(WM_ERROR, + "requestScrollCapture: caught exception dispatching callback: %s", e); + } finally { + Binder.restoreCallingIdentity(token); + } + } + @Override public void dontOverrideDisplayInfo(int displayId) { final long token = Binder.clearCallingIdentity(); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 83e7ad57e68d..ef690e1db396 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5289,7 +5289,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // to account for it. If we actually have shadows we will // then un-inset ourselves by the surfaceInsets. if (stack != null) { - final int outset = stack.getStackOutset(); + final int outset = stack.getTaskOutset(); outPoint.offset(outset, outset); } diff --git a/services/core/jni/com_android_server_tv_GamepadKeys.h b/services/core/jni/com_android_server_tv_GamepadKeys.h index 11fc9031da3b..127010f907ff 100644 --- a/services/core/jni/com_android_server_tv_GamepadKeys.h +++ b/services/core/jni/com_android_server_tv_GamepadKeys.h @@ -1,77 +1,104 @@ #ifndef ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_ #define ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_ +#include <android/input.h> +#include <android/keycodes.h> #include <linux/input.h> namespace android { -// Follows the W3 spec for gamepad buttons and their corresponding mapping into -// Linux keycodes. Note that gamepads are generally not very well standardized -// and various controllers will result in different buttons. This mapping tries -// to be reasonable. +// The constant array below defines a mapping between "Android" IDs (key code +// within events) and what is being sent through /dev/uinput. // -// W3 Button spec: https://www.w3.org/TR/gamepad/#remapping +// The translation back from uinput key codes into android key codes is done through +// the corresponding key layout files. This file and // -// Standard gamepad keycodes are added plus 2 additional buttons (e.g. Stadia -// has "Assistant" and "Share", PS4 has the touchpad button). +// data/keyboards/Vendor_18d1_Product_0200.kl // -// To generate this list, PS4, XBox, Stadia and Nintendo Switch Pro were tested. -static const int GAMEPAD_KEY_CODES[19] = { - // Right-side buttons. A/B/X/Y or circle/triangle/square/X or similar - BTN_A, // "South", A, GAMEPAD and SOUTH have the same constant - BTN_B, // "East", BTN_B, BTN_EAST have the same constant - BTN_X, // "West", Note that this maps to X and NORTH in constants - BTN_Y, // "North", Note that this maps to Y and WEST in constants +// MUST be kept in sync. +// +// see https://source.android.com/devices/input/key-layout-files for documentation. - BTN_TL, // "Left Bumper" / "L1" - Nintendo sends BTN_WEST instead - BTN_TR, // "Right Bumper" / "R1" - Nintendo sends BTN_Z instead +// Defines axis mapping information between android and +// uinput axis. +struct GamepadKey { + int32_t androidKeyCode; + int linuxUinputKeyCode; +}; + +static const GamepadKey GAMEPAD_KEYS[] = { + // Right-side buttons. A/B/X/Y or circle/triangle/square/X or similar + {AKEYCODE_BUTTON_A, BTN_A}, + {AKEYCODE_BUTTON_B, BTN_B}, + {AKEYCODE_BUTTON_X, BTN_X}, + {AKEYCODE_BUTTON_Y, BTN_Y}, - // For triggers, gamepads vary: - // - Stadia sends analog values over ABS_GAS/ABS_BRAKE and sends - // TriggerHappy3/4 as digital presses - // - PS4 and Xbox send analog values as ABS_Z/ABS_RZ - // - Nintendo Pro sends BTN_TL/BTN_TR (since bumpers behave differently) - // As placeholders we chose the stadia trigger-happy values since TL/TR are - // sent for bumper button presses - BTN_TRIGGER_HAPPY4, // "Left Trigger" / "L2" - BTN_TRIGGER_HAPPY3, // "Right Trigger" / "R2" + // Bumper buttons and digital triggers. Triggers generally have + // both analog versions (GAS and BRAKE output) and digital ones + {AKEYCODE_BUTTON_L1, BTN_TL2}, + {AKEYCODE_BUTTON_L2, BTN_TL}, + {AKEYCODE_BUTTON_R1, BTN_TR2}, + {AKEYCODE_BUTTON_R2, BTN_TR}, - BTN_SELECT, // "Select/Back". Often "options" or similar - BTN_START, // "Start/forward". Often "hamburger" icon + // general actions for controllers + {AKEYCODE_BUTTON_SELECT, BTN_SELECT}, // Options or "..." + {AKEYCODE_BUTTON_START, BTN_START}, // Menu/Hamburger menu + {AKEYCODE_BUTTON_MODE, BTN_MODE}, // "main" button - BTN_THUMBL, // "Left Joystick Pressed" - BTN_THUMBR, // "Right Joystick Pressed" + // Pressing on the joyticks themselves + {AKEYCODE_BUTTON_THUMBL, BTN_THUMBL}, + {AKEYCODE_BUTTON_THUMBR, BTN_THUMBR}, - // For DPads, gamepads generally only send axis changes - // on ABS_HAT0X and ABS_HAT0Y. - KEY_UP, // "Digital Pad up" - KEY_DOWN, // "Digital Pad down" - KEY_LEFT, // "Digital Pad left" - KEY_RIGHT, // "Digital Pad right" + // DPAD digital keys. HAT axis events are generally also sent. + {AKEYCODE_DPAD_UP, KEY_UP}, + {AKEYCODE_DPAD_DOWN, KEY_DOWN}, + {AKEYCODE_DPAD_LEFT, KEY_LEFT}, + {AKEYCODE_DPAD_RIGHT, KEY_RIGHT}, - BTN_MODE, // "Main button" (Stadia/PS/XBOX/Home) + // "Extra" controller buttons: some devices have "share" and "assistant" + {AKEYCODE_BUTTON_1, BTN_TRIGGER_HAPPY1}, + {AKEYCODE_BUTTON_2, BTN_TRIGGER_HAPPY2}, + {AKEYCODE_BUTTON_3, BTN_TRIGGER_HAPPY3}, + {AKEYCODE_BUTTON_4, BTN_TRIGGER_HAPPY4}, + {AKEYCODE_BUTTON_5, BTN_TRIGGER_HAPPY5}, + {AKEYCODE_BUTTON_6, BTN_TRIGGER_HAPPY6}, + {AKEYCODE_BUTTON_7, BTN_TRIGGER_HAPPY7}, + {AKEYCODE_BUTTON_8, BTN_TRIGGER_HAPPY8}, + {AKEYCODE_BUTTON_9, BTN_TRIGGER_HAPPY9}, + {AKEYCODE_BUTTON_10, BTN_TRIGGER_HAPPY10}, + {AKEYCODE_BUTTON_11, BTN_TRIGGER_HAPPY11}, + {AKEYCODE_BUTTON_12, BTN_TRIGGER_HAPPY12}, + {AKEYCODE_BUTTON_13, BTN_TRIGGER_HAPPY13}, + {AKEYCODE_BUTTON_14, BTN_TRIGGER_HAPPY14}, + {AKEYCODE_BUTTON_15, BTN_TRIGGER_HAPPY15}, + {AKEYCODE_BUTTON_16, BTN_TRIGGER_HAPPY16}, - BTN_TRIGGER_HAPPY1, // Extra button: "Assistant" for Stadia - BTN_TRIGGER_HAPPY2, // Extra button: "Share" for Stadia + // Assignment to support global assistant for devices that support it. + {AKEYCODE_ASSIST, KEY_ASSISTANT}, + {AKEYCODE_VOICE_ASSIST, KEY_VOICECOMMAND}, }; -// Defines information for an axis. -struct Axis { - int number; - int rangeMin; - int rangeMax; +// Defines axis mapping information between android and +// uinput axis. +struct GamepadAxis { + int32_t androidAxis; + float androidRangeMin; + float androidRangeMax; + int linuxUinputAxis; + int linuxUinputRangeMin; + int linuxUinputRangeMax; }; // List of all axes supported by a gamepad -static const Axis GAMEPAD_AXES[] = { - {ABS_X, 0, 254}, // Left joystick X - {ABS_Y, 0, 254}, // Left joystick Y - {ABS_RX, 0, 254}, // Right joystick X - {ABS_RY, 0, 254}, // Right joystick Y - {ABS_Z, 0, 254}, // Left trigger - {ABS_RZ, 0, 254}, // Right trigger - {ABS_HAT0X, -1, 1}, // DPad X - {ABS_HAT0Y, -1, 1}, // DPad Y +static const GamepadAxis GAMEPAD_AXES[] = { + {AMOTION_EVENT_AXIS_X, -1, 1, ABS_X, 0, 254}, // Left joystick X + {AMOTION_EVENT_AXIS_Y, -1, 1, ABS_Y, 0, 254}, // Left joystick Y + {AMOTION_EVENT_AXIS_Z, -1, 1, ABS_Z, 0, 254}, // Right joystick X + {AMOTION_EVENT_AXIS_RZ, -1, 1, ABS_RZ, 0, 254}, // Right joystick Y + {AMOTION_EVENT_AXIS_LTRIGGER, 0, 1, ABS_GAS, 0, 254}, // Left trigger + {AMOTION_EVENT_AXIS_RTRIGGER, 0, 1, ABS_BRAKE, 0, 254}, // Right trigger + {AMOTION_EVENT_AXIS_HAT_X, -1, 1, ABS_HAT0X, -1, 1}, // DPad X + {AMOTION_EVENT_AXIS_HAT_Y, -1, 1, ABS_HAT0Y, -1, 1}, // DPad Y }; } // namespace android diff --git a/services/core/jni/com_android_server_tv_TvUinputBridge.cpp b/services/core/jni/com_android_server_tv_TvUinputBridge.cpp index 0e96bd7ae47e..6e2e2c54518b 100644 --- a/services/core/jni/com_android_server_tv_TvUinputBridge.cpp +++ b/services/core/jni/com_android_server_tv_TvUinputBridge.cpp @@ -31,27 +31,38 @@ #include <utils/String8.h> #include <ctype.h> -#include <linux/input.h> -#include <unistd.h> -#include <sys/time.h> -#include <time.h> -#include <stdint.h> -#include <map> #include <fcntl.h> +#include <linux/input.h> #include <linux/uinput.h> #include <signal.h> +#include <stdint.h> #include <sys/inotify.h> #include <sys/stat.h> +#include <sys/time.h> #include <sys/types.h> +#include <time.h> +#include <unistd.h> +#include <unordered_map> #define SLOT_UNKNOWN -1 namespace android { -static std::map<int32_t,int> keysMap; -static std::map<int32_t,int32_t> slotsMap; +#define GOOGLE_VENDOR_ID 0x18d1 + +#define GOOGLE_VIRTUAL_REMOTE_PRODUCT_ID 0x0100 +#define GOOGLE_VIRTUAL_GAMEPAD_PROUCT_ID 0x0200 + +static std::unordered_map<int32_t, int> keysMap; +static std::unordered_map<int32_t, int32_t> slotsMap; static BitSet32 mtSlots; +// Maps android key code to linux key code. +static std::unordered_map<int32_t, int> gamepadAndroidToLinuxKeyMap; + +// Maps an android gamepad axis to the index within the GAMEPAD_AXES array. +static std::unordered_map<int32_t, int> gamepadAndroidAxisToIndexMap; + static void initKeysMap() { if (keysMap.empty()) { for (size_t i = 0; i < NELEM(KEYS); i++) { @@ -60,16 +71,49 @@ static void initKeysMap() { } } +static void initGamepadKeyMap() { + if (gamepadAndroidToLinuxKeyMap.empty()) { + for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) { + gamepadAndroidToLinuxKeyMap[GAMEPAD_KEYS[i].androidKeyCode] = + GAMEPAD_KEYS[i].linuxUinputKeyCode; + } + } + + if (gamepadAndroidAxisToIndexMap.empty()) { + for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) { + gamepadAndroidAxisToIndexMap[GAMEPAD_AXES[i].androidAxis] = i; + } + } +} + static int32_t getLinuxKeyCode(int32_t androidKeyCode) { - std::map<int,int>::iterator it = keysMap.find(androidKeyCode); + std::unordered_map<int, int>::iterator it = keysMap.find(androidKeyCode); if (it != keysMap.end()) { return it->second; } return KEY_UNKNOWN; } +static int getGamepadkeyCode(int32_t androidKeyCode) { + std::unordered_map<int32_t, int>::iterator it = + gamepadAndroidToLinuxKeyMap.find(androidKeyCode); + if (it != gamepadAndroidToLinuxKeyMap.end()) { + return it->second; + } + return KEY_UNKNOWN; +} + +static const GamepadAxis* getGamepadAxis(int32_t androidAxisCode) { + std::unordered_map<int32_t, int>::iterator it = + gamepadAndroidAxisToIndexMap.find(androidAxisCode); + if (it == gamepadAndroidToLinuxKeyMap.end()) { + return nullptr; + } + return &GAMEPAD_AXES[it->second]; +} + static int findSlot(int32_t pointerId) { - std::map<int,int>::iterator it = slotsMap.find(pointerId); + std::unordered_map<int, int>::iterator it = slotsMap.find(pointerId); if (it != slotsMap.end()) { return it->second; } @@ -107,7 +151,7 @@ public: // Open /dev/uinput and prepare to register // the device with the given name and unique Id - bool Open(const char* name, const char* uniqueId); + bool Open(const char* name, const char* uniqueId, uint16_t product); // Checks if the current file descriptor is valid bool IsValid() const { return mFd != kInvalidFileDescriptor; } @@ -141,7 +185,7 @@ int UInputDescriptor::Detach() { return fd; } -bool UInputDescriptor::Open(const char* name, const char* uniqueId) { +bool UInputDescriptor::Open(const char* name, const char* uniqueId, uint16_t product) { if (IsValid()) { ALOGE("UInput device already open"); return false; @@ -161,6 +205,8 @@ bool UInputDescriptor::Open(const char* name, const char* uniqueId) { strlcpy(mUinputDescriptor.name, name, UINPUT_MAX_NAME_SIZE); mUinputDescriptor.id.version = 1; mUinputDescriptor.id.bustype = BUS_VIRTUAL; + mUinputDescriptor.id.vendor = GOOGLE_VENDOR_ID; + mUinputDescriptor.id.product = product; // All UInput devices we use process keys ioctl(mFd, UI_SET_EVBIT, EV_KEY); @@ -258,7 +304,7 @@ NativeConnection* NativeConnection::open(const char* name, const char* uniqueId, initKeysMap(); UInputDescriptor descriptor; - if (!descriptor.Open(name, uniqueId)) { + if (!descriptor.Open(name, uniqueId, GOOGLE_VIRTUAL_REMOTE_PRODUCT_ID)) { return nullptr; } @@ -277,21 +323,24 @@ NativeConnection* NativeConnection::open(const char* name, const char* uniqueId, NativeConnection* NativeConnection::openGamepad(const char* name, const char* uniqueId) { ALOGI("Registering uinput device %s: gamepad", name); + initGamepadKeyMap(); + UInputDescriptor descriptor; - if (!descriptor.Open(name, uniqueId)) { + if (!descriptor.Open(name, uniqueId, GOOGLE_VIRTUAL_GAMEPAD_PROUCT_ID)) { return nullptr; } // set the keys mapped for gamepads - for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) { - descriptor.EnableKey(GAMEPAD_KEY_CODES[i]); + for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) { + descriptor.EnableKey(GAMEPAD_KEYS[i].linuxUinputKeyCode); } // define the axes that are required descriptor.EnableAxesEvents(); for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) { - const Axis& axis = GAMEPAD_AXES[i]; - descriptor.EnableAxis(axis.number, axis.rangeMin, axis.rangeMax); + const GamepadAxis& axis = GAMEPAD_AXES[i]; + descriptor.EnableAxis(axis.linuxUinputAxis, axis.linuxUinputRangeMin, + axis.linuxUinputRangeMax); } if (!descriptor.Create()) { @@ -350,7 +399,7 @@ static void nativeSendKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyCode, jb } } -static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyIndex, +static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyCode, jboolean down) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); @@ -359,16 +408,16 @@ static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyI return; } - if ((keyIndex < 0) || (keyIndex >= NELEM(GAMEPAD_KEY_CODES))) { - ALOGE("Invalid gamepad key index: %d", keyIndex); + int linuxKeyCode = getGamepadkeyCode(keyCode); + if (linuxKeyCode == KEY_UNKNOWN) { + ALOGE("Gamepad: received an unknown keycode of %d.", keyCode); return; } - - connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[keyIndex], down ? 1 : 0); + connection->sendEvent(EV_KEY, linuxKeyCode, down ? 1 : 0); } static void nativeSendGamepadAxisValue(JNIEnv* env, jclass clazz, jlong ptr, jint axis, - jint value) { + jfloat value) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); if (!connection->IsGamepad()) { @@ -376,7 +425,25 @@ static void nativeSendGamepadAxisValue(JNIEnv* env, jclass clazz, jlong ptr, jin return; } - connection->sendEvent(EV_ABS, axis, value); + const GamepadAxis* axisInfo = getGamepadAxis(axis); + if (axisInfo == nullptr) { + ALOGE("Invalid axis: %d", axis); + return; + } + + if (value > axisInfo->androidRangeMax) { + value = axisInfo->androidRangeMax; + } else if (value < axisInfo->androidRangeMin) { + value = axisInfo->androidRangeMin; + } + + // Converts the android range into the device range + float movementPercent = (value - axisInfo->androidRangeMin) / + (axisInfo->androidRangeMax - axisInfo->androidRangeMin); + int axisRawValue = axisInfo->linuxUinputRangeMin + + movementPercent * (axisInfo->linuxUinputRangeMax - axisInfo->linuxUinputRangeMin); + + connection->sendEvent(EV_ABS, axisInfo->linuxUinputAxis, axisRawValue); } static void nativeSendPointerDown(JNIEnv* env, jclass clazz, jlong ptr, @@ -441,18 +508,20 @@ static void nativeClear(JNIEnv* env, jclass clazz, jlong ptr) { } } } else { - for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) { - connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[i], 0); + for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) { + connection->sendEvent(EV_KEY, GAMEPAD_KEYS[i].linuxUinputKeyCode, 0); } for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) { - const Axis& axis = GAMEPAD_AXES[i]; - if ((axis.number == ABS_Z) || (axis.number == ABS_RZ)) { + const GamepadAxis& axis = GAMEPAD_AXES[i]; + + if ((axis.linuxUinputAxis == ABS_Z) || (axis.linuxUinputAxis == ABS_RZ)) { // Mark triggers unpressed - connection->sendEvent(EV_ABS, axis.number, 0); + connection->sendEvent(EV_ABS, axis.linuxUinputAxis, axis.linuxUinputRangeMin); } else { // Joysticks and dpad rests on center - connection->sendEvent(EV_ABS, axis.number, (axis.rangeMin + axis.rangeMax) / 2); + connection->sendEvent(EV_ABS, axis.linuxUinputAxis, + (axis.linuxUinputRangeMin + axis.linuxUinputRangeMax) / 2); } } } @@ -475,7 +544,7 @@ static JNINativeMethod gUinputBridgeMethods[] = { {"nativeClear", "(J)V", (void*)nativeClear}, {"nativeSendPointerSync", "(J)V", (void*)nativeSendPointerSync}, {"nativeSendGamepadKey", "(JIZ)V", (void*)nativeSendGamepadKey}, - {"nativeSendGamepadAxisValue", "(JII)V", (void*)nativeSendGamepadAxisValue}, + {"nativeSendGamepadAxisValue", "(JIF)V", (void*)nativeSendGamepadAxisValue}, }; int register_android_server_tv_TvUinputBridge(JNIEnv* env) { diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp index 79699bb77a92..f423119d240a 100644 --- a/services/incremental/IncrementalService.cpp +++ b/services/incremental/IncrementalService.cpp @@ -1610,6 +1610,8 @@ binder::Status IncrementalService::DataLoaderStub::onStatusChanged(MountId mount fsmStep(); + mStatusCondition.notify_all(); + return binder::Status::ok(); } diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java index f8bcff55ba41..6fe259e7fc85 100644 --- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -45,6 +45,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; @@ -54,9 +56,11 @@ import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.IApplicationThread; import android.app.IUidObserver; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -70,6 +74,7 @@ import androidx.test.filters.FlakyTest; import androidx.test.filters.MediumTest; import androidx.test.filters.SmallTest; +import com.android.server.LocalServices; import com.android.server.am.ProcessList.IsolatedUidRange; import com.android.server.am.ProcessList.IsolatedUidRangeAllocator; import com.android.server.appop.AppOpsService; @@ -77,6 +82,7 @@ import com.android.server.wm.ActivityTaskManagerService; import org.junit.After; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; @@ -115,6 +121,18 @@ public class ActivityManagerServiceTest { UidRecord.CHANGE_ACTIVE }; + private static PackageManagerInternal sPackageManagerInternal; + + @BeforeClass + public static void setUpOnce() { + sPackageManagerInternal = mock(PackageManagerInternal.class); + doReturn(new ComponentName("", "")).when(sPackageManagerInternal) + .getSystemUiServiceComponent(); + // Remove stale instance of PackageManagerInternal if there is any + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, sPackageManagerInternal); + } + @Rule public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule(); private Context mContext = getInstrumentation().getTargetContext(); @@ -258,8 +276,11 @@ public class ActivityManagerServiceTest { uidRec.hasInternetPermission = true; mAms.mProcessList.mActiveUids.put(uid, uidRec); - final ProcessRecord appRec = new ProcessRecord(mAms, new ApplicationInfo(), TAG, uid); - appRec.thread = Mockito.mock(IApplicationThread.class); + ApplicationInfo info = new ApplicationInfo(); + info.packageName = ""; + + final ProcessRecord appRec = new ProcessRecord(mAms, info, TAG, uid); + appRec.thread = mock(IApplicationThread.class); mAms.mProcessList.mLruProcesses.add(appRec); return uidRec; @@ -497,7 +518,7 @@ public class ActivityManagerServiceTest { }; final IUidObserver[] observers = new IUidObserver.Stub[changesToObserve.length]; for (int i = 0; i < observers.length; ++i) { - observers[i] = Mockito.mock(IUidObserver.Stub.class); + observers[i] = mock(IUidObserver.Stub.class); when(observers[i].asBinder()).thenReturn((IBinder) observers[i]); mAms.registerUidObserver(observers[i], changesToObserve[i] /* which */, ActivityManager.PROCESS_STATE_UNKNOWN /* cutpoint */, null /* caller */); @@ -610,7 +631,7 @@ public class ActivityManagerServiceTest { */ @Test public void testDispatchUidChanges_procStateCutpoint() throws RemoteException { - final IUidObserver observer = Mockito.mock(IUidObserver.Stub.class); + final IUidObserver observer = mock(IUidObserver.Stub.class); when(observer.asBinder()).thenReturn((IBinder) observer); mAms.registerUidObserver(observer, ActivityManager.UID_OBSERVER_PROCSTATE /* which */, @@ -704,7 +725,7 @@ public class ActivityManagerServiceTest { assertEquals("No observers registered, so validateUids should be empty", 0, mAms.mValidateUids.size()); - final IUidObserver observer = Mockito.mock(IUidObserver.Stub.class); + final IUidObserver observer = mock(IUidObserver.Stub.class); when(observer.asBinder()).thenReturn((IBinder) observer); mAms.registerUidObserver(observer, 0, 0, null); // Verify that when observers are registered, then validateUids is correctly updated. diff --git a/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java b/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java index d12d8040183a..b2d7177e04eb 100644 --- a/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java +++ b/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java @@ -22,12 +22,15 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import android.app.ActivityManager; import android.app.usage.UsageStatsManagerInternal; +import android.content.ComponentName; import android.content.Context; +import android.content.pm.PackageManagerInternal; import com.android.server.LocalServices; import com.android.server.wm.ActivityTaskManagerService; @@ -45,6 +48,7 @@ import org.junit.Test; public class OomAdjusterTests { private static Context sContext; private static ActivityManagerService sService; + private static PackageManagerInternal sPackageManagerInternal; private ProcessRecord mProcessRecord; @@ -56,6 +60,13 @@ public class OomAdjusterTests { public static void setUpOnce() { sContext = getInstrumentation().getTargetContext(); + sPackageManagerInternal = mock(PackageManagerInternal.class); + doReturn(new ComponentName("", "")).when(sPackageManagerInternal) + .getSystemUiServiceComponent(); + // Remove stale instance of PackageManagerInternal if there is any + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, sPackageManagerInternal); + // We need to run with dexmaker share class loader to make use of // ActivityTaskManagerService from wm package. runWithDexmakerShareClassLoader(() -> { diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java new file mode 100644 index 000000000000..c9dbdd2364cc --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings; + +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT; +import static com.android.server.locksettings.LockSettingsStrongAuth.DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS; +import static com.android.server.locksettings.LockSettingsStrongAuth.DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS; +import static com.android.server.locksettings.LockSettingsStrongAuth.NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG; +import static com.android.server.locksettings.LockSettingsStrongAuth.NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG; +import static com.android.server.locksettings.LockSettingsStrongAuth.STRONG_AUTH_TIMEOUT_ALARM_TAG; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AlarmManager; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import com.android.server.locksettings.LockSettingsStrongAuth.NonStrongBiometricIdleTimeoutAlarmListener; +import com.android.server.locksettings.LockSettingsStrongAuth.NonStrongBiometricTimeoutAlarmListener; +import com.android.server.locksettings.LockSettingsStrongAuth.StrongAuthTimeoutAlarmListener; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +public class LockSettingsStrongAuthTest { + + private static final String TAG = LockSettingsStrongAuthTest.class.getSimpleName(); + + private static final int PRIMARY_USER_ID = 0; + + private LockSettingsStrongAuth mStrongAuth; + private final int mDefaultStrongAuthFlags = STRONG_AUTH_NOT_REQUIRED; + private final boolean mDefaultIsNonStrongBiometricAllowed = true; + + @Mock + private Context mContext; + @Mock + private LockSettingsStrongAuth.Injector mInjector; + @Mock + private AlarmManager mAlarmManager; + @Mock + private DevicePolicyManager mDPM; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mInjector.getAlarmManager(mContext)).thenReturn(mAlarmManager); + when(mInjector.getDefaultStrongAuthFlags(mContext)).thenReturn(mDefaultStrongAuthFlags); + when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn(mDPM); + + mStrongAuth = new LockSettingsStrongAuth(mContext, mInjector); + } + + @Test + public void testScheduleNonStrongBiometricIdleTimeout() { + final long nextAlarmTime = 1000; + when(mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS)) + .thenReturn(nextAlarmTime); + mStrongAuth.scheduleNonStrongBiometricIdleTimeout(PRIMARY_USER_ID); + + waitForIdle(); + NonStrongBiometricIdleTimeoutAlarmListener alarm = mStrongAuth + .mNonStrongBiometricIdleTimeoutAlarmListener.get(PRIMARY_USER_ID); + // verify that a new alarm for idle timeout is added for the user + assertNotNull(alarm); + // verify that the alarm is scheduled + verifyAlarm(nextAlarmTime, NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG, alarm); + } + + @Test + public void testSetIsNonStrongBiometricAllowed_disallowed() { + mStrongAuth.setIsNonStrongBiometricAllowed(false /* allowed */, PRIMARY_USER_ID); + + waitForIdle(); + // verify that unlocking with non-strong biometrics is not allowed + assertFalse(mStrongAuth.mIsNonStrongBiometricAllowedForUser + .get(PRIMARY_USER_ID, mDefaultIsNonStrongBiometricAllowed)); + } + + @Test + public void testReportSuccessfulBiometricUnlock_nonStrongBiometric_fallbackTimeout() { + final long nextAlarmTime = 1000; + when(mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS)) + .thenReturn(nextAlarmTime); + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + + waitForIdle(); + NonStrongBiometricTimeoutAlarmListener alarm = + mStrongAuth.mNonStrongBiometricTimeoutAlarmListener.get(PRIMARY_USER_ID); + // verify that a new alarm for fallback timeout is added for the user + assertNotNull(alarm); + // verify that the alarm is scheduled + verifyAlarm(nextAlarmTime, NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG, alarm); + } + + @Test + public void testRequireStrongAuth_nonStrongBiometric_fallbackTimeout() { + mStrongAuth.requireStrongAuth( + STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT /* strongAuthReason */, + PRIMARY_USER_ID); + + waitForIdle(); + // verify that the StrongAuthFlags for the user contains the expected flag + final int expectedFlag = STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT; + verifyStrongAuthFlags(expectedFlag, PRIMARY_USER_ID); + } + + @Test + public void testReportSuccessfulBiometricUnlock_nonStrongBiometric_cancelIdleTimeout() { + // lock device and schedule an alarm for non-strong biometric idle timeout + mStrongAuth.scheduleNonStrongBiometricIdleTimeout(PRIMARY_USER_ID); + // unlock with non-strong biometric + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + + waitForIdle(); + + // verify that the current alarm for idle timeout is cancelled after a successful unlock + verify(mAlarmManager).cancel(any(NonStrongBiometricIdleTimeoutAlarmListener.class)); + } + + @Test + public void testReportSuccessfulBiometricUnlock_strongBio_cancelAlarmsAndAllowNonStrongBio() { + setupAlarms(PRIMARY_USER_ID); + mStrongAuth.reportSuccessfulBiometricUnlock(true /* isStrongBiometric */, PRIMARY_USER_ID); + + waitForIdle(); + // verify that unlocking with strong biometric cancels alarms for fallback and idle timeout + // and re-allow unlocking with non-strong biometric + verifyAlarmsCancelledAndNonStrongBiometricAllowed(PRIMARY_USER_ID); + } + + @Test + public void testReportSuccessfulStrongAuthUnlock_schedulePrimaryAuthTimeout() { + final long nextAlarmTime = 1000; + when(mInjector.getNextAlarmTimeMs(mDPM.getRequiredStrongAuthTimeout(null, PRIMARY_USER_ID))) + .thenReturn(nextAlarmTime); + mStrongAuth.reportSuccessfulStrongAuthUnlock(PRIMARY_USER_ID); + + waitForIdle(); + StrongAuthTimeoutAlarmListener alarm = + mStrongAuth.mStrongAuthTimeoutAlarmListenerForUser.get(PRIMARY_USER_ID); + // verify that a new alarm for primary auth timeout is added for the user + assertNotNull(alarm); + // verify that the alarm is scheduled + verifyAlarm(nextAlarmTime, STRONG_AUTH_TIMEOUT_ALARM_TAG, alarm); + } + + @Test + public void testReportSuccessfulStrongAuthUnlock_cancelAlarmsAndAllowNonStrongBio() { + setupAlarms(PRIMARY_USER_ID); + mStrongAuth.reportSuccessfulStrongAuthUnlock(PRIMARY_USER_ID); + + waitForIdle(); + // verify that unlocking with primary auth (PIN/pattern/password) cancels alarms + // for fallback and idle timeout and re-allow unlocking with non-strong biometric + verifyAlarmsCancelledAndNonStrongBiometricAllowed(PRIMARY_USER_ID); + } + + @Test + public void testFallbackTimeout_convenienceBiometric_weakBiometric() { + // assume that unlock with convenience biometric + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + // assume that unlock again with weak biometric + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + + waitForIdle(); + // verify that the fallback alarm scheduled when unlocking with convenience biometric is + // not affected when unlocking again with weak biometric + verify(mAlarmManager, never()).cancel(any(NonStrongBiometricTimeoutAlarmListener.class)); + assertNotNull(mStrongAuth.mNonStrongBiometricTimeoutAlarmListener.get(PRIMARY_USER_ID)); + } + + private void verifyAlarm(long when, String tag, AlarmManager.OnAlarmListener alarm) { + verify(mAlarmManager).set( + eq(AlarmManager.ELAPSED_REALTIME), + eq(when), + eq(tag), + eq(alarm), + eq(mStrongAuth.mHandler)); + } + + private void verifyStrongAuthFlags(int reason, int userId) { + final int flags = mStrongAuth.mStrongAuthForUser.get(userId, mDefaultStrongAuthFlags); + Log.d(TAG, "verifyStrongAuthFlags:" + + " reason=" + Integer.toHexString(reason) + + " userId=" + userId + + " flags=" + Integer.toHexString(flags)); + assertTrue(containsFlag(flags, reason)); + } + + private void setupAlarms(int userId) { + // schedule (a) an alarm for non-strong biometric fallback timeout and (b) an alarm for + // non-strong biometric idle timeout, so later we can verify that unlocking with + // strong biometric or primary auth will cancel those alarms + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + mStrongAuth.scheduleNonStrongBiometricIdleTimeout(PRIMARY_USER_ID); + } + + private void verifyAlarmsCancelledAndNonStrongBiometricAllowed(int userId) { + // verify that the current alarm for non-strong biometric fallback timeout is cancelled and + // removed + verify(mAlarmManager).cancel(any(NonStrongBiometricTimeoutAlarmListener.class)); + assertNull(mStrongAuth.mNonStrongBiometricTimeoutAlarmListener.get(userId)); + + // verify that the current alarm for non-strong biometric idle timeout is cancelled + verify(mAlarmManager).cancel(any(NonStrongBiometricIdleTimeoutAlarmListener.class)); + + // verify that unlocking with non-strong biometrics is allowed + assertTrue(mStrongAuth.mIsNonStrongBiometricAllowedForUser + .get(userId, mDefaultIsNonStrongBiometricAllowed)); + } + + private static boolean containsFlag(int haystack, int needle) { + return (haystack & needle) != 0; + } + + private static void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java index 0a6d3f3641fe..93ded1b6b2f3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java @@ -1196,7 +1196,7 @@ public class ActivityStackTests extends ActivityTestsBase { mDefaultTaskDisplayArea.registerStackOrderChangedListener(listener); try { mStack.mReparenting = true; - mDefaultTaskDisplayArea.addStack(mStack, 0); + mDefaultTaskDisplayArea.addChild(mStack, 0); } finally { mDefaultTaskDisplayArea.unregisterStackOrderChangedListener(listener); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java index 6ae8313e39dd..0700f9f2b29c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java @@ -329,6 +329,7 @@ class ActivityTestsBase extends SystemServiceTestsBase { private boolean mCreateStack = true; private ActivityStack mStack; + private TaskDisplayArea mTaskDisplayArea; TaskBuilder(ActivityStackSupervisor supervisor) { mSupervisor = supervisor; @@ -378,9 +379,16 @@ class ActivityTestsBase extends SystemServiceTestsBase { return this; } + TaskBuilder setDisplay(DisplayContent display) { + mTaskDisplayArea = display.getDefaultTaskDisplayArea(); + return this; + } + Task build() { if (mStack == null && mCreateStack) { - mStack = mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea().createStack( + TaskDisplayArea displayArea = mTaskDisplayArea != null ? mTaskDisplayArea + : mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea(); + mStack = displayArea.createStack( WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */); spyOn(mStack); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index daff14992e94..80fcf2e121f4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -41,6 +41,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; +import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; @@ -74,6 +75,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import android.annotation.SuppressLint; +import android.app.ActivityTaskManager; import android.app.WindowConfiguration; import android.content.res.Configuration; import android.graphics.Rect; @@ -1207,6 +1209,31 @@ public class DisplayContentTests extends WindowTestsBase { assertNull(taskDisplayArea.getOrCreateRootHomeTask()); } + @Test + public void testFindScrollCaptureTargetWindow_behindWindow() { + DisplayContent display = createNewDisplay(); + ActivityStack stack = createTaskStackOnDisplay(display); + Task task = createTaskInStack(stack, 0 /* userId */); + WindowState activityWindow = createAppWindow(task, TYPE_APPLICATION, "App Window"); + WindowState behindWindow = createWindow(null, TYPE_SCREENSHOT, display, "Screenshot"); + + WindowState result = display.findScrollCaptureTargetWindow(behindWindow, + ActivityTaskManager.INVALID_TASK_ID); + assertEquals(activityWindow, result); + } + + @Test + public void testFindScrollCaptureTargetWindow_taskId() { + DisplayContent display = createNewDisplay(); + ActivityStack stack = createTaskStackOnDisplay(display); + Task task = createTaskInStack(stack, 0 /* userId */); + WindowState window = createAppWindow(task, TYPE_APPLICATION, "App Window"); + WindowState behindWindow = createWindow(null, TYPE_SCREENSHOT, display, "Screenshot"); + + WindowState result = display.findScrollCaptureTargetWindow(null, task.mTaskId); + assertEquals(window, result); + } + private boolean isOptionsPanelAtRight(int displayId) { return (mWm.getPreferredOptionsPanelGravity(displayId) & Gravity.RIGHT) == Gravity.RIGHT; } diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java index be2559719438..e887be0c48c2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java @@ -33,6 +33,7 @@ import androidx.test.filters.SmallTest; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; /** * Build/Install/Run: @@ -40,6 +41,7 @@ import org.junit.Test; */ @SmallTest @Presubmit +@RunWith(WindowTestRunner.class) @FlakyTest public class RefreshRatePolicyTest extends WindowTestsBase { diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java index 519ac780bd6b..dcc2ff1311a5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java @@ -54,10 +54,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import android.app.ActivityManager; import android.app.TaskInfo; @@ -75,12 +72,10 @@ import android.util.DisplayMetrics; import android.util.Xml; import android.view.DisplayInfo; -import androidx.test.filters.FlakyTest; import androidx.test.filters.MediumTest; import com.android.internal.app.IVoiceInteractor; import com.android.server.wm.Task.TaskFactory; -import com.android.server.wm.utils.WmDisplayCutout; import org.junit.Before; import org.junit.Test; @@ -368,25 +363,38 @@ public class TaskRecordTests extends ActivityTestsBase { @Test public void testComputeConfigResourceOverrides() { - final Task task = new TaskBuilder(mSupervisor).build(); + final Rect fullScreenBounds = new Rect(0, 0, 1080, 1920); + TestDisplayContent display = new TestDisplayContent.Builder( + mService, fullScreenBounds.width(), fullScreenBounds.height()).build(); + final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build(); final Configuration inOutConfig = new Configuration(); final Configuration parentConfig = new Configuration(); final int longSide = 1200; final int shortSide = 600; + final Rect parentBounds = new Rect(0, 0, 250, 500); + parentConfig.windowConfiguration.setBounds(parentBounds); parentConfig.densityDpi = 400; - parentConfig.screenHeightDp = 200; // 200 * 400 / 160 = 500px - parentConfig.screenWidthDp = 100; // 100 * 400 / 160 = 250px + parentConfig.screenHeightDp = (parentBounds.bottom * 160) / parentConfig.densityDpi; // 200 + parentConfig.screenWidthDp = (parentBounds.right * 160) / parentConfig.densityDpi; // 100 parentConfig.windowConfiguration.setRotation(ROTATION_0); - // Portrait bounds. - inOutConfig.windowConfiguration.getBounds().set(0, 0, shortSide, longSide); - // By default, the parent bounds should limit the existing input bounds. + // By default, the input bounds will fill parent. task.computeConfigResourceOverrides(inOutConfig, parentConfig); assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp); assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp); assertEquals(Configuration.ORIENTATION_PORTRAIT, inOutConfig.orientation); + // If bounds are overridden, config properties should be made to match. Surface hierarchy + // will crop for policy. + inOutConfig.setToDefaults(); + inOutConfig.windowConfiguration.getBounds().set(0, 0, shortSide, longSide); + // By default, the parent bounds should limit the existing input bounds. + task.computeConfigResourceOverrides(inOutConfig, parentConfig); + + assertEquals(longSide, inOutConfig.screenHeightDp * parentConfig.densityDpi / 160); + assertEquals(shortSide, inOutConfig.screenWidthDp * parentConfig.densityDpi / 160); + inOutConfig.setToDefaults(); // Landscape bounds. inOutConfig.windowConfiguration.getBounds().set(0, 0, longSide, shortSide); @@ -394,21 +402,17 @@ public class TaskRecordTests extends ActivityTestsBase { // Setup the display with a top stable inset. The later assertion will ensure the inset is // excluded from screenHeightDp. final int statusBarHeight = 100; - final DisplayContent displayContent = task.mDisplayContent; - final DisplayPolicy policy = mock(DisplayPolicy.class); + final DisplayPolicy policy = display.getDisplayPolicy(); doAnswer(invocationOnMock -> { final Rect insets = invocationOnMock.<Rect>getArgument(0); insets.top = statusBarHeight; return null; }).when(policy).convertNonDecorInsetsToStableInsets(any(), eq(ROTATION_0)); - doReturn(policy).when(displayContent).getDisplayPolicy(); - doReturn(mock(WmDisplayCutout.class)).when(displayContent) - .calculateDisplayCutoutForRotation(anyInt()); // Without limiting to be inside the parent bounds, the out screen size should keep relative // to the input bounds. final ActivityRecord.CompatDisplayInsets compatIntsets = - new ActivityRecord.CompatDisplayInsets(displayContent, task); + new ActivityRecord.CompatDisplayInsets(display, task); task.computeConfigResourceOverrides(inOutConfig, parentConfig, compatIntsets); assertEquals((shortSide - statusBarHeight) * DENSITY_DEFAULT / parentConfig.densityDpi, @@ -454,7 +458,6 @@ public class TaskRecordTests extends ActivityTestsBase { parentConfig.screenWidthDp = 100; // 100 * 400 / 160 = 250px parentConfig.windowConfiguration.setRotation(ROTATION_0); - final float density = 2.5f; // densityDpi / DENSITY_DEFAULT_SCALE = 400 / 160.0f final int longSideDp = 480; // longSide / density = 1200 / 400 * 160 final int shortSideDp = 240; // shortSide / density = 600 / 400 * 160 final int screenLayout = parentConfig.screenLayout @@ -463,31 +466,38 @@ public class TaskRecordTests extends ActivityTestsBase { Configuration.reduceScreenLayout(screenLayout, longSideDp, shortSideDp); // Portrait bounds overlapping with navigation bar, without insets. - inOutConfig.windowConfiguration.getBounds().set(0, + final Rect freeformBounds = new Rect(0, displayHeight - 10 - longSide, shortSide, displayHeight - 10); + inOutConfig.windowConfiguration.setBounds(freeformBounds); // Set to freeform mode to verify bug fix. inOutConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); task.computeConfigResourceOverrides(inOutConfig, parentConfig); - assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp); - assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp); + // screenW/H should not be effected by parent since overridden and freeform + assertEquals(freeformBounds.width() * 160 / parentConfig.densityDpi, + inOutConfig.screenWidthDp); + assertEquals(freeformBounds.height() * 160 / parentConfig.densityDpi, + inOutConfig.screenHeightDp); assertEquals(reducedScreenLayout, inOutConfig.screenLayout); inOutConfig.setToDefaults(); // Landscape bounds overlapping with navigtion bar, without insets. - inOutConfig.windowConfiguration.getBounds().set(0, + freeformBounds.set(0, displayHeight - 10 - shortSide, longSide, displayHeight - 10); + inOutConfig.windowConfiguration.setBounds(freeformBounds); inOutConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); task.computeConfigResourceOverrides(inOutConfig, parentConfig); - assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp); - assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp); + assertEquals(freeformBounds.width() * 160 / parentConfig.densityDpi, + inOutConfig.screenWidthDp); + assertEquals(freeformBounds.height() * 160 / parentConfig.densityDpi, + inOutConfig.screenHeightDp); assertEquals(reducedScreenLayout, inOutConfig.screenLayout); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java index 7cb5e84e4e48..f354a04101f5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java @@ -186,7 +186,7 @@ public class TaskStackTests extends WindowTestsBase { final ActivityStack stack = createTaskStackOnDisplay(mDisplayContent); final int stackOutset = 10; spyOn(stack); - doReturn(stackOutset).when(stack).getStackOutset(); + doReturn(stackOutset).when(stack).getTaskOutset(); doReturn(true).when(stack).inMultiWindowMode(); // Mock the resolved override windowing mode to non-fullscreen diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java index 91c3c2782d94..e39b4bcd2eb0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java @@ -24,6 +24,7 @@ import android.os.RemoteException; import android.util.MergedConfiguration; import android.view.DisplayCutout; import android.view.DragEvent; +import android.view.IScrollCaptureController; import android.view.IWindow; import android.view.InsetsSourceControl; import android.view.InsetsState; @@ -113,6 +114,10 @@ public class TestIWindow extends IWindow.Stub { } @Override + public void requestScrollCapture(IScrollCaptureController controller) throws RemoteException { + } + + @Override public void showInsets(int types, boolean fromIme) throws RemoteException { } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 53cc09bf08e8..f65328dcbd42 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -44,7 +44,6 @@ import static com.android.server.wm.WindowContainer.POSITION_TOP; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -407,22 +406,20 @@ public class WindowOrganizerTests extends WindowTestsBase { .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); final Task task = stack.getTopMostTask(); WindowContainerTransaction t = new WindowContainerTransaction(); - t.setBounds(task.mRemoteToken.toWindowContainerToken(), new Rect(10, 10, 100, 100)); mWm.mAtmService.mWindowOrganizerController.applyTransaction(t); final int origScreenWDp = task.getConfiguration().screenHeightDp; final int origScreenHDp = task.getConfiguration().screenHeightDp; t = new WindowContainerTransaction(); // verify that setting config overrides on parent restricts children. t.setScreenSizeDp(stack.mRemoteToken - .toWindowContainerToken(), origScreenWDp, origScreenHDp); - t.setBounds(task.mRemoteToken.toWindowContainerToken(), new Rect(10, 10, 150, 200)); + .toWindowContainerToken(), origScreenWDp, origScreenHDp / 2); mWm.mAtmService.mWindowOrganizerController.applyTransaction(t); - assertEquals(origScreenHDp, task.getConfiguration().screenHeightDp); + assertEquals(origScreenHDp / 2, task.getConfiguration().screenHeightDp); t = new WindowContainerTransaction(); t.setScreenSizeDp(stack.mRemoteToken.toWindowContainerToken(), SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); mWm.mAtmService.mWindowOrganizerController.applyTransaction(t); - assertNotEquals(origScreenHDp, task.getConfiguration().screenHeightDp); + assertEquals(origScreenHDp, task.getConfiguration().screenHeightDp); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index e561c13a4e99..6a64d1c976c4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -247,7 +247,7 @@ class WindowTestsBase extends SystemServiceTestsBase { WindowState createAppWindow(Task task, int type, String name) { synchronized (mWm.mGlobalLock) { final ActivityRecord activity = - WindowTestUtils.createTestActivityRecord(mDisplayContent); + WindowTestUtils.createTestActivityRecord(task.getDisplayContent()); task.addChild(activity, 0); return createWindow(null, type, activity, name); } diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index bbe9851520a1..5b5d57bf2f51 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -1577,15 +1577,27 @@ public class UsageStatsService extends SystemService implements } @Override - public boolean isAppInactive(String packageName, int userId) { + public boolean isAppInactive(String packageName, int userId, String callingPackage) { + final int callingUid = Binder.getCallingUid(); try { userId = ActivityManager.getService().handleIncomingUser(Binder.getCallingPid(), - Binder.getCallingUid(), userId, false, false, "isAppInactive", null); + callingUid, userId, false, false, "isAppInactive", null); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } + + // If the calling app is asking about itself, continue, else check for permission. + if (packageName.equals(callingPackage)) { + final int actualCallingUid = mPackageManagerInternal.getPackageUidInternal( + callingPackage, 0, userId); + if (actualCallingUid != callingUid) { + return false; + } + } else if (!hasPermission(callingPackage)) { + return false; + } final boolean obfuscateInstantApps = shouldObfuscateInstantAppsForCaller( - Binder.getCallingUid(), userId); + callingUid, userId); final long token = Binder.clearCallingIdentity(); try { return mAppStandby.isAppIdleFiltered( diff --git a/telephony/java/com/android/internal/telephony/SmsHeader.java b/telephony/java/com/android/internal/telephony/SmsHeader.java index ab3fdf4ebb41..2f3897b9bac2 100644 --- a/telephony/java/com/android/internal/telephony/SmsHeader.java +++ b/telephony/java/com/android/internal/telephony/SmsHeader.java @@ -23,6 +23,8 @@ import com.android.internal.util.HexDump; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; /** * SMS user data header, as specified in TS 23.040 9.2.3.24. @@ -71,6 +73,25 @@ public class SmsHeader { public static final int PORT_WAP_PUSH = 2948; public static final int PORT_WAP_WSP = 9200; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SmsHeader smsHeader = (SmsHeader) o; + return languageTable == smsHeader.languageTable + && languageShiftTable == smsHeader.languageShiftTable + && Objects.equals(portAddrs, smsHeader.portAddrs) + && Objects.equals(concatRef, smsHeader.concatRef) + && Objects.equals(specialSmsMsgList, smsHeader.specialSmsMsgList) + && Objects.equals(miscEltList, smsHeader.miscEltList); + } + + @Override + public int hashCode() { + return Objects.hash(portAddrs, concatRef, specialSmsMsgList, miscEltList, languageTable, + languageShiftTable); + } + public static class PortAddrs { @UnsupportedAppUsage public PortAddrs() { @@ -81,6 +102,21 @@ public class SmsHeader { @UnsupportedAppUsage public int origPort; public boolean areEightBits; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PortAddrs portAddrs = (PortAddrs) o; + return destPort == portAddrs.destPort + && origPort == portAddrs.origPort + && areEightBits == portAddrs.areEightBits; + } + + @Override + public int hashCode() { + return Objects.hash(destPort, origPort, areEightBits); + } } public static class ConcatRef { @@ -95,11 +131,41 @@ public class SmsHeader { @UnsupportedAppUsage public int msgCount; public boolean isEightBits; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConcatRef concatRef = (ConcatRef) o; + return refNumber == concatRef.refNumber + && seqNumber == concatRef.seqNumber + && msgCount == concatRef.msgCount + && isEightBits == concatRef.isEightBits; + } + + @Override + public int hashCode() { + return Objects.hash(refNumber, seqNumber, msgCount, isEightBits); + } } public static class SpecialSmsMsg { public int msgIndType; public int msgCount; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SpecialSmsMsg that = (SpecialSmsMsg) o; + return msgIndType == that.msgIndType + && msgCount == that.msgCount; + } + + @Override + public int hashCode() { + return Objects.hash(msgIndType, msgCount); + } } /** @@ -109,6 +175,22 @@ public class SmsHeader { public static class MiscElt { public int id; public byte[] data; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MiscElt miscElt = (MiscElt) o; + return id == miscElt.id + && Arrays.equals(data, miscElt.data); + } + + @Override + public int hashCode() { + int result = Objects.hash(id); + result = 31 * result + Arrays.hashCode(data); + return result; + } } @UnsupportedAppUsage diff --git a/tests/AutoVerify/app1/Android.bp b/tests/AutoVerify/app1/Android.bp new file mode 100644 index 000000000000..548519fa653b --- /dev/null +++ b/tests/AutoVerify/app1/Android.bp @@ -0,0 +1,11 @@ +android_app { + name: "AutoVerifyTest", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + min_sdk_version: "26", + target_sdk_version: "26", + optimize: { + enabled: false, + }, +} diff --git a/tests/AutoVerify/app1/AndroidManifest.xml b/tests/AutoVerify/app1/AndroidManifest.xml new file mode 100644 index 000000000000..d9caad490d82 --- /dev/null +++ b/tests/AutoVerify/app1/AndroidManifest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.test.autoverify" > + + <uses-sdk android:targetSdkVersion="26" /> + + <application + android:label="@string/app_name" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <intent-filter android:autoVerify="true"> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="explicit.example.com" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/AutoVerify/app1/res/values/strings.xml b/tests/AutoVerify/app1/res/values/strings.xml new file mode 100644 index 000000000000..e234355041c6 --- /dev/null +++ b/tests/AutoVerify/app1/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2020 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. +--> + +<resources> + <!-- app icon label, do not translate --> + <string name="app_name" translatable="false">AutoVerify Test</string> +</resources> diff --git a/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java new file mode 100644 index 000000000000..09ef47212622 --- /dev/null +++ b/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2020 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. + */ diff --git a/tests/AutoVerify/app2/Android.bp b/tests/AutoVerify/app2/Android.bp new file mode 100644 index 000000000000..1c6c97bdf350 --- /dev/null +++ b/tests/AutoVerify/app2/Android.bp @@ -0,0 +1,11 @@ +android_app { + name: "AutoVerifyTest2", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + min_sdk_version: "26", + target_sdk_version: "26", + optimize: { + enabled: false, + }, +} diff --git a/tests/AutoVerify/app2/AndroidManifest.xml b/tests/AutoVerify/app2/AndroidManifest.xml new file mode 100644 index 000000000000..a00807883cfc --- /dev/null +++ b/tests/AutoVerify/app2/AndroidManifest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.test.autoverify" > + + <uses-sdk android:targetSdkVersion="26" /> + + <application + android:label="@string/app_name" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <intent-filter android:autoVerify="true"> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="explicit.example.com" /> + <data android:host="*.wildcard.tld" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/AutoVerify/app2/res/values/strings.xml b/tests/AutoVerify/app2/res/values/strings.xml new file mode 100644 index 000000000000..e234355041c6 --- /dev/null +++ b/tests/AutoVerify/app2/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2020 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. +--> + +<resources> + <!-- app icon label, do not translate --> + <string name="app_name" translatable="false">AutoVerify Test</string> +</resources> diff --git a/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java new file mode 100644 index 000000000000..09ef47212622 --- /dev/null +++ b/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2020 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. + */ diff --git a/tests/AutoVerify/app3/Android.bp b/tests/AutoVerify/app3/Android.bp new file mode 100644 index 000000000000..70a2b77d1000 --- /dev/null +++ b/tests/AutoVerify/app3/Android.bp @@ -0,0 +1,11 @@ +android_app { + name: "AutoVerifyTest3", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + min_sdk_version: "26", + target_sdk_version: "26", + optimize: { + enabled: false, + }, +} diff --git a/tests/AutoVerify/app3/AndroidManifest.xml b/tests/AutoVerify/app3/AndroidManifest.xml new file mode 100644 index 000000000000..efaabc9a38d3 --- /dev/null +++ b/tests/AutoVerify/app3/AndroidManifest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.test.autoverify" > + + <uses-sdk android:targetSdkVersion="26" /> + + <application + android:label="@string/app_name" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <!-- does not request autoVerify --> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="explicit.example.com" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/AutoVerify/app3/res/values/strings.xml b/tests/AutoVerify/app3/res/values/strings.xml new file mode 100644 index 000000000000..e234355041c6 --- /dev/null +++ b/tests/AutoVerify/app3/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2020 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. +--> + +<resources> + <!-- app icon label, do not translate --> + <string name="app_name" translatable="false">AutoVerify Test</string> +</resources> diff --git a/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java new file mode 100644 index 000000000000..09ef47212622 --- /dev/null +++ b/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2020 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. + */ diff --git a/tests/AutoVerify/app4/Android.bp b/tests/AutoVerify/app4/Android.bp new file mode 100644 index 000000000000..fbdae1181a7a --- /dev/null +++ b/tests/AutoVerify/app4/Android.bp @@ -0,0 +1,11 @@ +android_app { + name: "AutoVerifyTest4", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + min_sdk_version: "26", + target_sdk_version: "26", + optimize: { + enabled: false, + }, +} diff --git a/tests/AutoVerify/app4/AndroidManifest.xml b/tests/AutoVerify/app4/AndroidManifest.xml new file mode 100644 index 000000000000..1c975f8336c9 --- /dev/null +++ b/tests/AutoVerify/app4/AndroidManifest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.test.autoverify" > + + <uses-sdk android:targetSdkVersion="26" /> + + <application + android:label="@string/app_name" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <!-- intentionally does not autoVerify --> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="explicit.example.com" /> + <data android:host="*.wildcard.tld" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/AutoVerify/app4/res/values/strings.xml b/tests/AutoVerify/app4/res/values/strings.xml new file mode 100644 index 000000000000..e234355041c6 --- /dev/null +++ b/tests/AutoVerify/app4/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2020 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. +--> + +<resources> + <!-- app icon label, do not translate --> + <string name="app_name" translatable="false">AutoVerify Test</string> +</resources> diff --git a/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java new file mode 100644 index 000000000000..09ef47212622 --- /dev/null +++ b/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2020 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. + */ |