Merge "Update default for conversations in DND" into rvc-dev
diff --git a/apex/media/framework/java/android/media/MediaParser.java b/apex/media/framework/java/android/media/MediaParser.java
index d1e9e4e..50f4ddd 100644
--- a/apex/media/framework/java/android/media/MediaParser.java
+++ b/apex/media/framework/java/android/media/MediaParser.java
@@ -1412,14 +1412,12 @@
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 @@
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 103b53e..c268ff4 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 @@
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/api/test-current.txt b/api/test-current.txt
index 8f3d042..755380e 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -151,6 +151,7 @@
ctor public ActivityView(android.content.Context, android.util.AttributeSet);
ctor public ActivityView(android.content.Context, android.util.AttributeSet, int);
ctor public ActivityView(android.content.Context, android.util.AttributeSet, int, boolean);
+ ctor public ActivityView(@NonNull android.content.Context, @NonNull android.util.AttributeSet, int, boolean, boolean);
method public int getVirtualDisplayId();
method public void onLayout(boolean, int, int, int, int);
method public void onLocationChanged();
diff --git a/cmds/screencap/screencap.cpp b/cmds/screencap/screencap.cpp
index 4410f1c..bb32dd2 100644
--- a/cmds/screencap/screencap.cpp
+++ b/cmds/screencap/screencap.cpp
@@ -105,7 +105,7 @@
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/core/java/android/app/ActivityView.java b/core/java/android/app/ActivityView.java
index ab7925c..1098fa1 100644
--- a/core/java/android/app/ActivityView.java
+++ b/core/java/android/app/ActivityView.java
@@ -90,11 +90,23 @@
public ActivityView(Context context, AttributeSet attrs, int defStyle,
boolean singleTaskInstance) {
+ this(context, attrs, defStyle, singleTaskInstance, false /* usePublicVirtualDisplay */);
+ }
+
+ /**
+ * This constructor let's the caller explicitly request a public virtual display as the backing
+ * display. Using a public display is not recommended as it exposes it to other applications,
+ * but it might be needed for backwards compatibility.
+ */
+ public ActivityView(
+ @NonNull Context context, @NonNull AttributeSet attrs, int defStyle,
+ boolean singleTaskInstance, boolean usePublicVirtualDisplay) {
super(context, attrs, defStyle);
if (useTaskOrganizer()) {
mTaskEmbedder = new TaskOrganizerTaskEmbedder(context, this);
} else {
- mTaskEmbedder = new VirtualDisplayTaskEmbedder(context, this, singleTaskInstance);
+ mTaskEmbedder = new VirtualDisplayTaskEmbedder(context, this, singleTaskInstance,
+ usePublicVirtualDisplay);
}
mSurfaceView = new SurfaceView(context);
// Since ActivityView#getAlpha has been overridden, we should use parent class's alpha
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 8e0d939..37f683e 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -233,12 +233,9 @@
}
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 @@
* @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 1211c73..ed6ba0c 100644
--- a/core/java/android/app/usage/IUsageStatsManager.aidl
+++ b/core/java/android/app/usage/IUsageStatsManager.aidl
@@ -43,7 +43,7 @@
@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 0a67802..2ce6a86 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -622,12 +622,17 @@
* 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/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index ca86157..1fef071 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -897,6 +897,34 @@
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/view/IScrollCaptureClient.aidl b/core/java/android/view/IScrollCaptureClient.aidl
new file mode 100644
index 0000000..5f135a37
--- /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 0000000..8474a00
--- /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 9f2d36d..e09bf9d 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -21,15 +21,16 @@
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 @@
* 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 b0bacb9..b3b53f0 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -42,6 +42,7 @@
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 @@
* @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 0000000..e1a4e74
--- /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 0000000..f163124
--- /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 0000000..628e23f
--- /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 0000000..f3fcabb
--- /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 0000000..71e82c5
--- /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 8abe72f..f98c1f6 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -144,6 +144,7 @@
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.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 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 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 @@
* 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 @@
*/
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 @@
* 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 @@
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 @@
int mLeashedParentAccessibilityViewId;
/**
+ *
+ */
+ ScrollCaptureInternal mScrollCaptureInternal;
+
+ /**
* Creates a new set of attachment information with the specified
* events handler and thread.
*
@@ -29150,6 +29226,14 @@
return events;
}
+
+ @Nullable
+ ScrollCaptureInternal getScrollCaptureInternal() {
+ if (mScrollCaptureInternal != null) {
+ mScrollCaptureInternal = new ScrollCaptureInternal();
+ }
+ return mScrollCaptureInternal;
+ }
}
/**
@@ -29683,6 +29767,104 @@
}
}
+
+ /**
+ * 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 e34e84c..7935eb1 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -40,6 +40,7 @@
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.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Queue;
import java.util.function.Predicate;
/**
@@ -188,7 +190,16 @@
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 @@
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 @@
}
}
- 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 @@
@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 @@
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 @@
}
/**
+ * 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 ed1edc3..68a185d 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -206,6 +206,7 @@
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 @@
private final InsetsController mInsetsController;
private final ImeFocusController mImeFocusController;
+ private ScrollCaptureClient mScrollCaptureClient;
+
/**
* @return {@link ImeFocusController} for this instance.
*/
@@ -661,6 +664,11 @@
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 @@
// draw returns.
private SurfaceControl.Transaction mRtBLASTSyncTransaction = new SurfaceControl.Transaction();
+ private HashSet<ScrollCaptureCallback> mRootScrollCaptureCallbacks;
+
private String mTag = TAG;
public ViewRootImpl(Context context, Display display) {
@@ -4778,6 +4788,7 @@
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 +5091,9 @@
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 +8803,131 @@
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 +9230,13 @@
}
}
+ @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 ae9afaba..b153648 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -2535,6 +2535,33 @@
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 ec51301..397bce4 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -24,7 +24,6 @@
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/android/window/VirtualDisplayTaskEmbedder.java b/core/java/android/window/VirtualDisplayTaskEmbedder.java
index 7016469..1c0598b 100644
--- a/core/java/android/window/VirtualDisplayTaskEmbedder.java
+++ b/core/java/android/window/VirtualDisplayTaskEmbedder.java
@@ -64,6 +64,7 @@
// For Virtual Displays
private int mDisplayDensityDpi;
private final boolean mSingleTaskInstance;
+ private final boolean mUsePublicVirtualDisplay;
private VirtualDisplay mVirtualDisplay;
private Insets mForwardedInsets;
private DisplayMetrics mTmpDisplayMetrics;
@@ -78,9 +79,10 @@
* only applicable if virtual displays are used
*/
public VirtualDisplayTaskEmbedder(Context context, VirtualDisplayTaskEmbedder.Host host,
- boolean singleTaskInstance) {
+ boolean singleTaskInstance, boolean usePublicVirtualDisplay) {
super(context, host);
mSingleTaskInstance = singleTaskInstance;
+ mUsePublicVirtualDisplay = usePublicVirtualDisplay;
}
/**
@@ -97,11 +99,16 @@
public boolean onInitialize() {
final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
mDisplayDensityDpi = getBaseDisplayDensity();
+
+ int virtualDisplayFlags = VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
+ | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL;
+ if (mUsePublicVirtualDisplay) {
+ virtualDisplayFlags |= VIRTUAL_DISPLAY_FLAG_PUBLIC;
+ }
+
mVirtualDisplay = displayManager.createVirtualDisplay(
DISPLAY_NAME + "@" + System.identityHashCode(this), mHost.getWidth(),
- mHost.getHeight(), mDisplayDensityDpi, null,
- VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
- | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL);
+ mHost.getHeight(), mDisplayDensityDpi, null, virtualDisplayFlags);
if (mVirtualDisplay == null) {
Log.e(TAG, "Failed to initialize TaskEmbedder");
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index d851a09..970bab9 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -2611,11 +2611,12 @@
* 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 36eecfb..3eb0923 100644
--- a/core/java/com/android/internal/app/IntentForwarderActivity.java
+++ b/core/java/com/android/internal/app/IntentForwarderActivity.java
@@ -163,7 +163,7 @@
return;
}
sanitizeIntent(innerIntent);
- startActivity(intentReceived);
+ startActivityAsCaller(intentReceived, null, null, false, getUserId());
finish();
}
@@ -234,23 +234,7 @@
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 25c114f..23ba653 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.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.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 @@
: 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 47f094f..7f3eb45 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.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 @@
@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 0000000..c589afde
--- /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 0000000..9f100bd
--- /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 0000000..4087eda
--- /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 0000000..12bd461
--- /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/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 4065a6c..a8d1605 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 67d20da..fb887c3 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/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java
index ea778fd..a354f1d 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.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 @@
@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 @@
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 @@
@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 @@
}
}
+ @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 @@
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 0000000..e6ac2d6
--- /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 0000000..8b21b8e
--- /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 0000000..3af0533
--- /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 769c578..5424b6f 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 @@
}
@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 0000000..63a68e9
--- /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 18086ec..c5ac451 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 0000000..231fac6
--- /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 0000000..d30bcc6
--- /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/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index cbf2364..1bfa999 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -1312,7 +1312,7 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
}
/**
- * 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/tv/ITvRemoteServiceInput.aidl b/media/java/android/media/tv/ITvRemoteServiceInput.aidl
index a0b6c9b..0e6563a 100644
--- a/media/java/android/media/tv/ITvRemoteServiceInput.aidl
+++ b/media/java/android/media/tv/ITvRemoteServiceInput.aidl
@@ -39,4 +39,10 @@
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 0bf0f97..b97ac26 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.util.Log;
import java.util.LinkedList;
+import java.util.Objects;
/**
* Base class for emote providers implemented in unbundled service.
@@ -124,27 +127,75 @@
* @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 " + name + ": failure", re);
+ Log.e(TAG, "Delayed openRemoteInputBridge() for " + finalName
+ + ": failure", re);
}
});
return;
}
}
try {
- mRemoteServiceInput.openInputBridge(token, name, width, height, maxPointers);
- Log.d(TAG, "openRemoteInputBridge() for " + name + ": success");
+ 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 openGamepadBridge() for " + finalName + ": failure",
+ re);
+ }
+ });
+ return;
+ }
+ }
+ try {
+ mRemoteServiceInput.openGamepadBridge(token, finalName);
+ Log.d(TAG, "openGamepadBridge() for " + finalName + ": success");
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
@@ -157,6 +208,7 @@
* @throws RuntimeException
*/
public void closeInputBridge(IBinder token) throws RuntimeException {
+ Objects.requireNonNull(token);
try {
mRemoteServiceInput.closeInputBridge(token);
} catch (RemoteException re) {
@@ -173,6 +225,7 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 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 @@
* @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 @@
}
}
+ /**
+ * 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 c9ce5613..e6e3939 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 @@
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/packages/CarSystemUI/res/layout/car_qs_footer.xml b/packages/CarSystemUI/res/layout/car_qs_footer.xml
deleted file mode 100644
index bf96c00..0000000
--- 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 0c6f322..0000000
--- 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 81c7108..12c9f11 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 7972e09..3e44721 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 5f33f8f..0000000
--- 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 f68d034..9014eb1 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 e7ecf7f..0000000
--- 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
similarity index 100%
rename from packages/CarSystemUI/res/values/ids_car.xml
rename to packages/CarSystemUI/res/values/ids.xml
diff --git a/packages/CarSystemUI/res/values/integers.xml b/packages/CarSystemUI/res/values/integers.xml
index 8b87c74..5ae5555 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 db8ce954..0000000
--- 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 9ea7ed0..881e746 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 83e91c5..0000000
--- 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
similarity index 95%
rename from packages/CarSystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java
rename to packages/CarSystemUI/src/com/android/systemui/car/userswitcher/CarStatusBarHeader.java
index 4ef926f..bab6715 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/qs/car/CarQSFooter.java b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java
deleted file mode 100644
index b74f199..0000000
--- 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 31965c5..0000000
--- 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 d8111d0..ec1dabc 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.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 @@
@Override
protected QS createDefaultQSFragment() {
- return new CarQSFragment();
+ return null;
}
private BatteryController createBatteryController() {
diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp
index 587109d..54986a4 100644
--- a/packages/CtsShim/build/Android.bp
+++ b/packages/CtsShim/build/Android.bp
@@ -69,6 +69,14 @@
// 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 @@
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 @@
},
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 ea15b43..7a5b07e 100644
--- a/packages/CtsShim/build/jni/Android.bp
+++ b/packages/CtsShim/build/jni/Android.bp
@@ -18,4 +18,13 @@
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/SystemUI/res/layout/global_actions_grid_v2.xml b/packages/SystemUI/res/layout/global_actions_grid_v2.xml
index dba003a..1c4ec64 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 aa2fe3c..57b3761 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.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 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 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 718bcf1..65bf7e6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -152,6 +152,11 @@
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 502c078..9cfcc52 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -227,6 +227,13 @@
}
@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 20b1e0d..43cef3a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
@@ -159,4 +159,9 @@
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 a7c4043..8f3dc22 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 @@
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 @@
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 2c1bd21..ea358c7 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.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 @@
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);
}
}
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 0000000..5ced40c
--- /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/stackdivider/WindowManagerProxy.java b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
index 85dcbb6..5aa7946 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 @@
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 37fc13e..afb5002 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 @@
res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications);
mBubbleController = bubbleController;
mDynamicPrivacyController = privacyController;
- privacyController.addListener(this);
mDynamicChildBindController = dynamicChildBindController;
}
@@ -131,6 +130,7 @@
NotificationListContainer listContainer) {
mPresenter = presenter;
mListContainer = listContainer;
+ mDynamicPrivacyController.addListener(this);
}
/**
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 82e02b4..39949c8 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 @@
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 e5da603..899aabb 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.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 @@
@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 25f279b..dd5c833 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 @@
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/statusbar/phone/KeyguardBouncerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
index e052ae2..0a041e4 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.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.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.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import org.mockito.stubbing.Answer;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -94,9 +95,11 @@
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 @@
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 @@
mKeyguardBypassController, mHandler) {
@Override
protected void inflateView() {
- super.inflateView();
mKeyguardView = mKeyguardHostView;
- mRoot = spy(mRoot);
- mRootView = mRoot;
+ mRoot = mRootView;
}
};
}
@@ -212,8 +218,10 @@
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/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 9d1ad42..b27c5d5 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 @@
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 @@
*/
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 @@
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 @@
return false;
}
- return mInlineSessionController.onInlineSuggestionsResponseLocked(mCurrentViewId,
+ return mInlineSessionController.onInlineSuggestionsResponseLocked(focusedId,
inlineSuggestionsResponse);
}
@@ -3107,19 +3114,19 @@
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 @@
remoteRenderService.getInlineSuggestionsRendererInfo(new RemoteCallback(
(extras) -> {
mInlineSessionController.onCreateInlineSuggestionsRequestLocked(
- mCurrentViewId, /*requestConsumer=*/ requestAugmentedAutofill,
+ focusedId, /*requestConsumer=*/ requestAugmentedAutofill,
extras);
}, mHandler));
} else {
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index f1ea5d0..552331e 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -5889,6 +5889,7 @@
private void processLinkPropertiesFromAgent(NetworkAgentInfo nai, LinkProperties lp) {
lp.ensureDirectlyConnectedRoutes();
+ nai.clatd.setNat64PrefixFromRa(lp.getNat64Prefix());
}
private void updateLinkProperties(NetworkAgentInfo networkAgent, LinkProperties newLp,
diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java
index ac4a42c..e066d99 100644
--- a/services/core/java/com/android/server/VibratorService.java
+++ b/services/core/java/com/android/server/VibratorService.java
@@ -1034,6 +1034,9 @@
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 8f5fbf7..149e3ba 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 @@
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/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index cce749d..e28464a 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -2240,10 +2240,11 @@
if (mVoldAppDataIsolationEnabled && UserHandle.isApp(app.uid)
&& !storageManagerInternal.isExternalStorageService(uid)) {
bindMountAppStorageDirs = true;
- if (!storageManagerInternal.prepareStorageDirs(userId, pkgDataInfoMap.keySet(),
+ if (pkgDataInfoMap == null ||
+ !storageManagerInternal.prepareStorageDirs(userId, pkgDataInfoMap.keySet(),
app.processName)) {
- // Cannot prepare Android/app and Android/obb directory,
- // so we won't mount it in zygote.
+ // Cannot prepare Android/app and Android/obb directory or inode == 0,
+ // so we won't mount it in zygote, but resume the mount after unlocking device.
app.bindMountPending = true;
bindMountAppStorageDirs = false;
}
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 97d3b17..1139fb2 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -5750,7 +5750,7 @@
*/
private void switchPackageIfBootTimeOrRarelyUsedLocked(@NonNull String packageName) {
if (mSampledPackage == null) {
- if (ThreadLocalRandom.current().nextFloat() < 0.1f) {
+ if (ThreadLocalRandom.current().nextFloat() < 0.5f) {
mSamplingStrategy = SAMPLING_STRATEGY_BOOT_TIME_SAMPLING;
resampleAppOpForPackageLocked(packageName);
}
diff --git a/services/core/java/com/android/server/connectivity/Nat464Xlat.java b/services/core/java/com/android/server/connectivity/Nat464Xlat.java
index e6b2d26..34d0bed 100644
--- a/services/core/java/com/android/server/connectivity/Nat464Xlat.java
+++ b/services/core/java/com/android/server/connectivity/Nat464Xlat.java
@@ -81,15 +81,23 @@
RUNNING, // start() called, and the stacked iface is known to be up.
}
- /** NAT64 prefix currently in use. Only valid in STARTING or RUNNING states. */
+ /**
+ * NAT64 prefix currently in use. Only valid in STARTING or RUNNING states.
+ * Used, among other things, to avoid updates when switching from a prefix learned from one
+ * source (e.g., RA) to the same prefix learned from another source (e.g., RA).
+ */
private IpPrefix mNat64PrefixInUse;
/** NAT64 prefix (if any) discovered from DNS via RFC 7050. */
private IpPrefix mNat64PrefixFromDns;
+ /** NAT64 prefix (if any) learned from the network via RA. */
+ private IpPrefix mNat64PrefixFromRa;
private String mBaseIface;
private String mIface;
private Inet6Address mIPv6Address;
private State mState = State.IDLE;
+ private boolean mPrefixDiscoveryRunning;
+
public Nat464Xlat(NetworkAgentInfo nai, INetd netd, IDnsResolver dnsResolver,
INetworkManagementService nmService) {
mDnsResolver = dnsResolver;
@@ -139,15 +147,6 @@
}
/**
- * @return true if we have started prefix discovery and not yet stopped it (regardless of
- * whether it is still running or has succeeded).
- * A true result corresponds to internal states DISCOVERING, STARTING and RUNNING.
- */
- public boolean isPrefixDiscoveryStarted() {
- return mState == State.DISCOVERING || isStarted();
- }
-
- /**
* @return true if clatd has been started and has not yet stopped.
* A true result corresponds to internal states STARTING and RUNNING.
*/
@@ -181,7 +180,7 @@
return;
}
- mNat64PrefixInUse = getNat64Prefix();
+ mNat64PrefixInUse = selectNat64Prefix();
String addrStr = null;
try {
addrStr = mNetd.clatdStart(baseIface, mNat64PrefixInUse.toString());
@@ -196,6 +195,9 @@
} catch (ClassCastException | IllegalArgumentException | NullPointerException e) {
Slog.e(TAG, "Invalid IPv6 address " + addrStr);
}
+ if (mPrefixDiscoveryRunning && !isPrefixDiscoveryNeeded()) {
+ stopPrefixDiscovery();
+ }
}
/**
@@ -218,10 +220,15 @@
mNat64PrefixInUse = null;
mIface = null;
mBaseIface = null;
- if (requiresClat(mNetwork)) {
+
+ if (isPrefixDiscoveryNeeded()) {
+ if (!mPrefixDiscoveryRunning) {
+ startPrefixDiscovery();
+ }
mState = State.DISCOVERING;
} else {
- stopPrefixDiscovery(); // Enters IDLE state.
+ stopPrefixDiscovery();
+ mState = State.IDLE;
}
}
@@ -282,7 +289,7 @@
} catch (RemoteException | ServiceSpecificException e) {
Slog.e(TAG, "Error starting prefix discovery on netId " + getNetId() + ": " + e);
}
- mState = State.DISCOVERING;
+ mPrefixDiscoveryRunning = true;
}
private void stopPrefixDiscovery() {
@@ -291,11 +298,18 @@
} catch (RemoteException | ServiceSpecificException e) {
Slog.e(TAG, "Error stopping prefix discovery on netId " + getNetId() + ": " + e);
}
- mState = State.IDLE;
+ mPrefixDiscoveryRunning = false;
+ }
+
+ private boolean isPrefixDiscoveryNeeded() {
+ // If there is no NAT64 prefix in the RA, prefix discovery is always needed. It cannot be
+ // stopped after it succeeds, because stopping it will cause netd to report that the prefix
+ // has been removed, and that will cause us to stop clatd.
+ return requiresClat(mNetwork) && mNat64PrefixFromRa == null;
}
private void maybeHandleNat64PrefixChange() {
- final IpPrefix newPrefix = getNat64Prefix();
+ final IpPrefix newPrefix = selectNat64Prefix();
if (!Objects.equals(mNat64PrefixInUse, newPrefix)) {
Slog.d(TAG, "NAT64 prefix changed from " + mNat64PrefixInUse + " to "
+ newPrefix);
@@ -314,13 +328,11 @@
// TODO: turn this class into a proper StateMachine. http://b/126113090
switch (mState) {
case IDLE:
- if (requiresClat(mNetwork)) {
- // Network is detected to be IPv6-only.
- // TODO: consider going to STARTING directly if the NAT64 prefix is already
- // known. This would however result in clatd running without prefix discovery
- // running, which might be a surprising combination.
+ if (isPrefixDiscoveryNeeded()) {
startPrefixDiscovery(); // Enters DISCOVERING state.
- return;
+ mState = State.DISCOVERING;
+ } else if (requiresClat(mNetwork)) {
+ start(); // Enters STARTING state.
}
break;
@@ -333,6 +345,7 @@
if (!requiresClat(mNetwork)) {
// IPv4 address added. Go back to IDLE state.
stopPrefixDiscovery();
+ mState = State.IDLE;
return;
}
break;
@@ -351,8 +364,20 @@
}
}
- private IpPrefix getNat64Prefix() {
- return mNat64PrefixFromDns;
+ /**
+ * Picks a NAT64 prefix to use. Always prefers the prefix from the RA if one is received from
+ * both RA and DNS, because the prefix in the RA has better security and updatability, and will
+ * almost always be received first anyway.
+ *
+ * Any network that supports legacy hosts will support discovering the DNS64 prefix via DNS as
+ * well. If the prefix from the RA is withdrawn, fall back to that for reliability purposes.
+ */
+ private IpPrefix selectNat64Prefix() {
+ return mNat64PrefixFromRa != null ? mNat64PrefixFromRa : mNat64PrefixFromDns;
+ }
+
+ public void setNat64PrefixFromRa(IpPrefix prefix) {
+ mNat64PrefixFromRa = prefix;
}
public void setNat64PrefixFromDns(IpPrefix prefix) {
@@ -367,7 +392,7 @@
public void fixupLinkProperties(@NonNull LinkProperties oldLp, @NonNull LinkProperties lp) {
// This must be done even if clatd is not running, because otherwise shouldStartClat would
// never return true.
- lp.setNat64Prefix(getNat64Prefix());
+ lp.setNat64Prefix(selectNat64Prefix());
if (!isRunning()) {
return;
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java
index fbee6f4..8480197 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.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.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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
}
- 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 @@
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 @@
}
// 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 @@
/**
* 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 @@
/**
* 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 @@
/**
* 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 @@
}
}
- 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/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index e5867e7..476c9f8 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -165,6 +165,10 @@
mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance(mContext);
mAudioPlayerStateMonitor.registerListener(
(config, isRemoved) -> {
+ if (DEBUG) {
+ Log.d(TAG, "Audio playback is changed, config=" + config
+ + ", removed=" + isRemoved);
+ }
if (config.getPlayerType()
== AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
return;
@@ -1993,7 +1997,7 @@
FullUserRecord user = getFullUserRecordLocked(record.getUserId());
if (record != null && user != null) {
record.setSessionPolicies(policies);
- user.mPriorityStack.updateMediaButtonSessionIfNeeded();
+ user.mPriorityStack.updateMediaButtonSessionBySessionPolicyChange(record);
}
}
} finally {
diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java
index bd98b9c..402355a 100644
--- a/services/core/java/com/android/server/media/MediaSessionStack.java
+++ b/services/core/java/com/android/server/media/MediaSessionStack.java
@@ -16,6 +16,8 @@
package com.android.server.media;
+import static com.android.server.media.SessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_SESSION;
+
import android.media.Session2Token;
import android.media.session.MediaSession;
import android.os.Debug;
@@ -102,6 +104,7 @@
// When the media button session is removed, nullify the media button session and do not
// search for the alternative media session within the app. It's because the alternative
// media session might be a dummy which isn't able to handle the media key events.
+ // TODO(b/154456172): Make this decision unaltered by non-media app's playback.
updateMediaButtonSession(null);
}
clearCache(record.getUserId());
@@ -158,7 +161,7 @@
findMediaButtonSession(mMediaButtonSession.getUid());
if (newMediaButtonSession != mMediaButtonSession
&& (newMediaButtonSession.getSessionPolicies()
- & SessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_SESSION) == 0) {
+ & SESSION_POLICY_IGNORE_BUTTON_SESSION) == 0) {
// Check if the policy states that this session should not be updated as a media
// button session.
updateMediaButtonSession(newMediaButtonSession);
@@ -189,19 +192,43 @@
}
IntArray audioPlaybackUids = mAudioPlayerStateMonitor.getSortedAudioPlaybackClientUids();
for (int i = 0; i < audioPlaybackUids.size(); i++) {
- MediaSessionRecordImpl mediaButtonSession =
- findMediaButtonSession(audioPlaybackUids.get(i));
- if (mediaButtonSession == null) continue;
- boolean ignoreButtonSession = (mediaButtonSession.getSessionPolicies()
- & SessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_SESSION) != 0;
- if (mediaButtonSession == mMediaButtonSession && ignoreButtonSession) {
+ int audioPlaybackUid = audioPlaybackUids.get(i);
+ MediaSessionRecordImpl mediaButtonSession = findMediaButtonSession(audioPlaybackUid);
+ if (mediaButtonSession == null) {
+ if (DEBUG) {
+ Log.d(TAG, "updateMediaButtonSessionIfNeeded, skipping uid="
+ + audioPlaybackUid);
+ }
+ // Ignore if the lastly played app isn't a media app (i.e. has no media session)
+ continue;
+ }
+ boolean ignoreButtonSession =
+ (mediaButtonSession.getSessionPolicies()
+ & SESSION_POLICY_IGNORE_BUTTON_SESSION) != 0;
+ if (DEBUG) {
+ Log.d(TAG, "updateMediaButtonSessionIfNeeded, checking uid=" + audioPlaybackUid
+ + ", mediaButtonSession=" + mediaButtonSession
+ + ", ignoreButtonSession=" + ignoreButtonSession);
+ }
+ if (!ignoreButtonSession) {
+ mAudioPlayerStateMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid());
+ if (mediaButtonSession != mMediaButtonSession) {
+ updateMediaButtonSession(mediaButtonSession);
+ }
+ return;
+ }
+ }
+ }
+
+ // TODO: Remove this and make updateMediaButtonSessionIfNeeded() to also cover this case.
+ public void updateMediaButtonSessionBySessionPolicyChange(MediaSessionRecord record) {
+ if ((record.getSessionPolicies() & SESSION_POLICY_IGNORE_BUTTON_SESSION) != 0) {
+ if (record == mMediaButtonSession) {
+ // TODO(b/154456172): Make this decision unaltered by non-media app's playback.
updateMediaButtonSession(null);
- return;
}
- if (mediaButtonSession != mMediaButtonSession && !ignoreButtonSession) {
- updateMediaButtonSession(mediaButtonSession);
- return;
- }
+ } else {
+ updateMediaButtonSessionIfNeeded();
}
}
@@ -280,7 +307,7 @@
return mMediaButtonSession;
}
- private void updateMediaButtonSession(MediaSessionRecordImpl newMediaButtonSession) {
+ public void updateMediaButtonSession(MediaSessionRecordImpl newMediaButtonSession) {
MediaSessionRecordImpl oldMediaButtonSession = mMediaButtonSession;
mMediaButtonSession = newMediaButtonSession;
mOnMediaButtonSessionChangedListener.onMediaButtonSessionChanged(
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 07078f2..9f543b4 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -1346,13 +1346,6 @@
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 +17632,120 @@
+ " 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 +18450,7 @@
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 +19543,14 @@
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 +19559,8 @@
}
}
} else {
- if (mSettings.removeIntentFilterVerificationLPw(packageName, userId)) {
+ if (mSettings.removeIntentFilterVerificationLPw(packageName, userId,
+ alsoResetStatus)) {
scheduleWritePackageRestrictionsLocked(userId);
}
}
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 53c057a..44a61d8 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 @@
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 @@
}
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/power/batterysaver/BatterySaverPolicy.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java
index 059861b..701197e 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 @@
* 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 @@
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 @@
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 @@
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 @@
@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 @@
} else {
locationMode = rawPolicy.locationMode;
}
- mEffectivePolicy = new Policy(
+
+ mEffectivePolicyRaw = new Policy(
rawPolicy.adjustBrightnessFactor,
rawPolicy.advertiseIsEnabled,
rawPolicy.deferFullBackup,
@@ -489,24 +487,24 @@
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 @@
*/
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 @@
}
mAdaptivePolicy = p;
- if (getPolicyLevelLocked() == POLICY_LEVEL_ADAPTIVE) {
+ if (mPolicyLevel == POLICY_LEVEL_ADAPTIVE) {
updatePolicyDependenciesLocked();
return true;
}
@@ -1011,11 +1009,11 @@
}
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 @@
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 @@
}
}
}
-
- /** 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 8fe6da5..390340a 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 @@
}
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
}
}
+
+ @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 a2fe5fc..1dc201d 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 @@
/** 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 @@
* @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/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index a47cdc6..b01acca 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -5450,6 +5450,46 @@
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/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index cb9b332..25791c7 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 @@
@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 @@
}
}
- 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 @@
}
} 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 @@
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 899ab24..f3e2992 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 @@
// 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 f55a1b3..a501414 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.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 @@
}
}
+ /**
+ * 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/jni/com_android_server_tv_GamepadKeys.h b/services/core/jni/com_android_server_tv_GamepadKeys.h
index 11fc903..127010f 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
-
- // 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"
-
- BTN_SELECT, // "Select/Back". Often "options" or similar
- BTN_START, // "Start/forward". Often "hamburger" icon
-
- BTN_THUMBL, // "Left Joystick Pressed"
- BTN_THUMBR, // "Right Joystick Pressed"
-
- // 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"
-
- BTN_MODE, // "Main button" (Stadia/PS/XBOX/Home)
-
- BTN_TRIGGER_HAPPY1, // Extra button: "Assistant" for Stadia
- BTN_TRIGGER_HAPPY2, // Extra button: "Share" for Stadia
+// Defines axis mapping information between android and
+// uinput axis.
+struct GamepadKey {
+ int32_t androidKeyCode;
+ int linuxUinputKeyCode;
};
-// Defines information for an axis.
-struct Axis {
- int number;
- int rangeMin;
- int rangeMax;
+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},
+
+ // 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},
+
+ // 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
+
+ // Pressing on the joyticks themselves
+ {AKEYCODE_BUTTON_THUMBL, BTN_THUMBL},
+ {AKEYCODE_BUTTON_THUMBR, BTN_THUMBR},
+
+ // 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},
+
+ // "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},
+
+ // Assignment to support global assistant for devices that support it.
+ {AKEYCODE_ASSIST, KEY_ASSISTANT},
+ {AKEYCODE_VOICE_ASSIST, KEY_VOICECOMMAND},
+};
+
+// 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 0e96bd7..6e2e2c5 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 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 @@
// 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 @@
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 @@
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 @@
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::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 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 @@
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 @@
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 @@
}
}
} 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 @@
{"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/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 0000000..c9dbdd2
--- /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 0a6d3f3..93ded1b 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 @@
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/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index daff149..80fcf2e 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_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.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 @@
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/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
index 91c3c27..e39b4bc 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.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 @@
}
@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/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index e561c13..6a64d1c 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 @@
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 bbe9851..5b5d57b 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 @@
}
@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 ab3fdf4..2f3897b 100644
--- a/telephony/java/com/android/internal/telephony/SmsHeader.java
+++ b/telephony/java/com/android/internal/telephony/SmsHeader.java
@@ -23,6 +23,8 @@
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 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 @@
@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 @@
@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 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 0000000..548519f
--- /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 0000000..d9caad4
--- /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 0000000..e234355
--- /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 0000000..09ef472
--- /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 0000000..1c6c97b
--- /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 0000000..a008078
--- /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 0000000..e234355
--- /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 0000000..09ef472
--- /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 0000000..70a2b77
--- /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 0000000..efaabc9
--- /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 0000000..e234355
--- /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 0000000..09ef472
--- /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 0000000..fbdae11
--- /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 0000000..1c975f8
--- /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 0000000..e234355
--- /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 0000000..09ef472
--- /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.
+ */
diff --git a/tests/net/common/java/android/net/NetworkProviderTest.kt b/tests/net/common/java/android/net/NetworkProviderTest.kt
index 4601c4b..b7c47c2 100644
--- a/tests/net/common/java/android/net/NetworkProviderTest.kt
+++ b/tests/net/common/java/android/net/NetworkProviderTest.kt
@@ -105,14 +105,43 @@
.build()
val cb = ConnectivityManager.NetworkCallback()
mCm.requestNetwork(nr, cb)
- provider.expectCallback<OnNetworkRequested>() {
- callback -> callback.request.getNetworkSpecifier() == specifier &&
+ provider.expectCallback<OnNetworkRequested>() { callback ->
+ callback.request.getNetworkSpecifier() == specifier &&
callback.request.hasTransport(TRANSPORT_TEST)
}
+ val initialScore = 40
+ val updatedScore = 60
+ val nc = NetworkCapabilities().apply {
+ addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+ removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ setNetworkSpecifier(specifier)
+ }
+ val lp = LinkProperties()
+ val config = NetworkAgentConfig.Builder().build()
+ val agent = object : NetworkAgent(context, mHandlerThread.looper, "TestAgent", nc, lp,
+ initialScore, config, provider) {}
+
+ provider.expectCallback<OnNetworkRequested>() { callback ->
+ callback.request.getNetworkSpecifier() == specifier &&
+ callback.score == initialScore &&
+ callback.id == agent.providerId
+ }
+
+ agent.sendNetworkScore(updatedScore)
+ provider.expectCallback<OnNetworkRequested>() { callback ->
+ callback.request.getNetworkSpecifier() == specifier &&
+ callback.score == updatedScore &&
+ callback.id == agent.providerId
+ }
+
mCm.unregisterNetworkCallback(cb)
- provider.expectCallback<OnNetworkRequestWithdrawn>() {
- callback -> callback.request.getNetworkSpecifier() == specifier &&
+ provider.expectCallback<OnNetworkRequestWithdrawn>() { callback ->
+ callback.request.getNetworkSpecifier() == specifier &&
callback.request.hasTransport(TRANSPORT_TEST)
}
mCm.unregisterNetworkProvider(provider)
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index dad0363..a478e68 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -6179,6 +6179,111 @@
mCm.unregisterNetworkCallback(networkCallback);
}
+ private void expectNat64PrefixChange(TestableNetworkCallback callback,
+ TestNetworkAgentWrapper agent, IpPrefix prefix) {
+ callback.expectLinkPropertiesThat(agent, x -> Objects.equals(x.getNat64Prefix(), prefix));
+ }
+
+ @Test
+ public void testNat64PrefixMultipleSources() throws Exception {
+ final String iface = "wlan0";
+ final String pref64FromRaStr = "64:ff9b::";
+ final String pref64FromDnsStr = "2001:db8:64::";
+ final IpPrefix pref64FromRa = new IpPrefix(InetAddress.getByName(pref64FromRaStr), 96);
+ final IpPrefix pref64FromDns = new IpPrefix(InetAddress.getByName(pref64FromDnsStr), 96);
+ final IpPrefix newPref64FromRa = new IpPrefix("2001:db8:64:64:64:64::/96");
+
+ final NetworkRequest request = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build();
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.registerNetworkCallback(request, callback);
+
+ final LinkProperties baseLp = new LinkProperties();
+ baseLp.setInterfaceName(iface);
+ baseLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64"));
+ baseLp.addDnsServer(InetAddress.getByName("2001:4860:4860::6464"));
+
+ reset(mMockNetd, mMockDnsResolver);
+ InOrder inOrder = inOrder(mMockNetd, mMockDnsResolver);
+
+ // If a network already has a NAT64 prefix on connect, clatd is started immediately and
+ // prefix discovery is never started.
+ LinkProperties lp = new LinkProperties(baseLp);
+ lp.setNat64Prefix(pref64FromRa);
+ mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp);
+ mCellNetworkAgent.connect(false);
+ final Network network = mCellNetworkAgent.getNetwork();
+ int netId = network.getNetId();
+ callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+ inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+ inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+ callback.assertNoCallback();
+ assertEquals(pref64FromRa, mCm.getLinkProperties(network).getNat64Prefix());
+
+ // If the RA prefix is withdrawn, clatd is stopped and prefix discovery is started.
+ lp.setNat64Prefix(null);
+ mCellNetworkAgent.sendLinkProperties(lp);
+ expectNat64PrefixChange(callback, mCellNetworkAgent, null);
+ inOrder.verify(mMockNetd).clatdStop(iface);
+ inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
+
+ // If the RA prefix appears while DNS discovery is in progress, discovery is stopped and
+ // clatd is started with the prefix from the RA.
+ lp.setNat64Prefix(pref64FromRa);
+ mCellNetworkAgent.sendLinkProperties(lp);
+ expectNat64PrefixChange(callback, mCellNetworkAgent, pref64FromRa);
+ inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+ inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
+
+ // Withdraw the RA prefix so we can test the case where an RA prefix appears after DNS
+ // discovery has succeeded.
+ lp.setNat64Prefix(null);
+ mCellNetworkAgent.sendLinkProperties(lp);
+ expectNat64PrefixChange(callback, mCellNetworkAgent, null);
+ inOrder.verify(mMockNetd).clatdStop(iface);
+ inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
+
+ mService.mNetdEventCallback.onNat64PrefixEvent(netId, true /* added */,
+ pref64FromDnsStr, 96);
+ expectNat64PrefixChange(callback, mCellNetworkAgent, pref64FromDns);
+ inOrder.verify(mMockNetd).clatdStart(iface, pref64FromDns.toString());
+
+ // If the RA prefix reappears, clatd is restarted and prefix discovery is stopped.
+ lp.setNat64Prefix(pref64FromRa);
+ mCellNetworkAgent.sendLinkProperties(lp);
+ expectNat64PrefixChange(callback, mCellNetworkAgent, pref64FromRa);
+ inOrder.verify(mMockNetd).clatdStop(iface);
+ inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
+ inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+ inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+
+ // If the RA prefix changes, clatd is restarted and prefix discovery is not started.
+ lp.setNat64Prefix(newPref64FromRa);
+ mCellNetworkAgent.sendLinkProperties(lp);
+ expectNat64PrefixChange(callback, mCellNetworkAgent, newPref64FromRa);
+ inOrder.verify(mMockNetd).clatdStop(iface);
+ inOrder.verify(mMockNetd).clatdStart(iface, newPref64FromRa.toString());
+ inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
+ inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+
+ // If the RA prefix changes to the same value, nothing happens.
+ lp.setNat64Prefix(newPref64FromRa);
+ mCellNetworkAgent.sendLinkProperties(lp);
+ callback.assertNoCallback();
+ assertEquals(newPref64FromRa, mCm.getLinkProperties(network).getNat64Prefix());
+ inOrder.verify(mMockNetd, never()).clatdStop(iface);
+ inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+ inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
+ inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+
+ // The transition between no prefix and DNS prefix is tested in testStackedLinkProperties.
+
+ callback.assertNoCallback();
+ mCellNetworkAgent.disconnect();
+ mCm.unregisterNetworkCallback(callback);
+ }
+
@Test
public void testDataActivityTracking() throws Exception {
final TestNetworkCallback networkCallback = new TestNetworkCallback();
diff --git a/tests/net/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/net/java/com/android/server/connectivity/Nat464XlatTest.java
index d0ebb52..5046b65 100644
--- a/tests/net/java/com/android/server/connectivity/Nat464XlatTest.java
+++ b/tests/net/java/com/android/server/connectivity/Nat464XlatTest.java
@@ -58,8 +58,10 @@
static final String BASE_IFACE = "test0";
static final String STACKED_IFACE = "v4-test0";
+ static final LinkAddress V6ADDR = new LinkAddress("2001:db8:1::f00/64");
static final LinkAddress ADDR = new LinkAddress("192.0.2.5/29");
static final String NAT64_PREFIX = "64:ff9b::/96";
+ static final String OTHER_NAT64_PREFIX = "2001:db8:0:64::/96";
static final int NETID = 42;
@Mock ConnectivityService mConnectivity;
@@ -81,6 +83,14 @@
};
}
+ private void markNetworkConnected() {
+ mNai.networkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTED, "", "");
+ }
+
+ private void markNetworkDisconnected() {
+ mNai.networkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED, "", "");
+ }
+
@Before
public void setUp() throws Exception {
mLooper = new TestLooper();
@@ -92,6 +102,7 @@
mNai.linkProperties.setInterfaceName(BASE_IFACE);
mNai.networkInfo = new NetworkInfo(null);
mNai.networkInfo.setType(ConnectivityManager.TYPE_WIFI);
+ markNetworkConnected();
when(mNai.connService()).thenReturn(mConnectivity);
when(mNai.netAgentConfig()).thenReturn(mAgentConfig);
when(mNai.handler()).thenReturn(mHandler);
@@ -139,7 +150,7 @@
for (NetworkInfo.DetailedState state : supportedDetailedStates) {
mNai.networkInfo.setDetailedState(state, "reason", "extraInfo");
- mNai.linkProperties.setNat64Prefix(new IpPrefix("2001:db8:0:64::/96"));
+ mNai.linkProperties.setNat64Prefix(new IpPrefix(OTHER_NAT64_PREFIX));
assertRequiresClat(false, mNai);
assertShouldStartClat(false, mNai);
@@ -176,11 +187,20 @@
}
}
- @Test
- public void testNormalStartAndStop() throws Exception {
+ private void makeClatUnnecessary(boolean dueToDisconnect) {
+ if (dueToDisconnect) {
+ markNetworkDisconnected();
+ } else {
+ mNai.linkProperties.addLinkAddress(ADDR);
+ }
+ }
+
+ private void checkNormalStartAndStop(boolean dueToDisconnect) throws Exception {
Nat464Xlat nat = makeNat464Xlat();
ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
+ mNai.linkProperties.addLinkAddress(V6ADDR);
+
nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
// Start clat.
@@ -200,6 +220,7 @@
assertRunning(nat);
// Stop clat (Network disconnects, IPv4 addr appears, ...).
+ makeClatUnnecessary(dueToDisconnect);
nat.stop();
verify(mNetd).clatdStop(eq(BASE_IFACE));
@@ -217,11 +238,23 @@
verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
}
+ @Test
+ public void testNormalStartAndStopDueToDisconnect() throws Exception {
+ checkNormalStartAndStop(true);
+ }
+
+ @Test
+ public void testNormalStartAndStopDueToIpv4Addr() throws Exception {
+ checkNormalStartAndStop(false);
+ }
+
private void checkStartStopStart(boolean interfaceRemovedFirst) throws Exception {
Nat464Xlat nat = makeNat464Xlat();
ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
InOrder inOrder = inOrder(mNetd, mConnectivity);
+ mNai.linkProperties.addLinkAddress(V6ADDR);
+
nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
nat.start();
@@ -344,10 +377,11 @@
verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
}
- @Test
- public void testStopBeforeClatdStarts() throws Exception {
+ private void checkStopBeforeClatdStarts(boolean dueToDisconnect) throws Exception {
Nat464Xlat nat = makeNat464Xlat();
+ mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+
nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
nat.start();
@@ -356,6 +390,7 @@
verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
// ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
+ makeClatUnnecessary(dueToDisconnect);
nat.stop();
verify(mNetd).clatdStop(eq(BASE_IFACE));
@@ -377,9 +412,20 @@
}
@Test
- public void testStopAndClatdNeverStarts() throws Exception {
+ public void testStopDueToDisconnectBeforeClatdStarts() throws Exception {
+ checkStopBeforeClatdStarts(true);
+ }
+
+ @Test
+ public void testStopDueToIpv4AddrBeforeClatdStarts() throws Exception {
+ checkStopBeforeClatdStarts(false);
+ }
+
+ private void checkStopAndClatdNeverStarts(boolean dueToDisconnect) throws Exception {
Nat464Xlat nat = makeNat464Xlat();
+ mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+
nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
nat.start();
@@ -388,6 +434,7 @@
verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
// ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
+ makeClatUnnecessary(dueToDisconnect);
nat.stop();
verify(mNetd).clatdStop(eq(BASE_IFACE));
@@ -398,6 +445,57 @@
verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
}
+ @Test
+ public void testStopDueToDisconnectAndClatdNeverStarts() throws Exception {
+ checkStopAndClatdNeverStarts(true);
+ }
+
+ @Test
+ public void testStopDueToIpv4AddressAndClatdNeverStarts() throws Exception {
+ checkStopAndClatdNeverStarts(false);
+ }
+
+ @Test
+ public void testNat64PrefixPreference() throws Exception {
+ final IpPrefix prefixFromDns = new IpPrefix(NAT64_PREFIX);
+ final IpPrefix prefixFromRa = new IpPrefix(OTHER_NAT64_PREFIX);
+
+ Nat464Xlat nat = makeNat464Xlat();
+
+ final LinkProperties emptyLp = new LinkProperties();
+ LinkProperties fixedupLp;
+
+ fixedupLp = new LinkProperties();
+ nat.setNat64PrefixFromDns(prefixFromDns);
+ nat.fixupLinkProperties(emptyLp, fixedupLp);
+ assertEquals(prefixFromDns, fixedupLp.getNat64Prefix());
+
+ fixedupLp = new LinkProperties();
+ nat.setNat64PrefixFromRa(prefixFromRa);
+ nat.fixupLinkProperties(emptyLp, fixedupLp);
+ assertEquals(prefixFromRa, fixedupLp.getNat64Prefix());
+
+ fixedupLp = new LinkProperties();
+ nat.setNat64PrefixFromRa(null);
+ nat.fixupLinkProperties(emptyLp, fixedupLp);
+ assertEquals(prefixFromDns, fixedupLp.getNat64Prefix());
+
+ fixedupLp = new LinkProperties();
+ nat.setNat64PrefixFromRa(prefixFromRa);
+ nat.fixupLinkProperties(emptyLp, fixedupLp);
+ assertEquals(prefixFromRa, fixedupLp.getNat64Prefix());
+
+ fixedupLp = new LinkProperties();
+ nat.setNat64PrefixFromDns(null);
+ nat.fixupLinkProperties(emptyLp, fixedupLp);
+ assertEquals(prefixFromRa, fixedupLp.getNat64Prefix());
+
+ fixedupLp = new LinkProperties();
+ nat.setNat64PrefixFromRa(null);
+ nat.fixupLinkProperties(emptyLp, fixedupLp);
+ assertEquals(null, fixedupLp.getNat64Prefix());
+ }
+
static void assertIdle(Nat464Xlat nat) {
assertTrue("Nat464Xlat was not IDLE", !nat.isStarted());
}