diff options
54 files changed, 1962 insertions, 878 deletions
diff --git a/core/java/android/os/UserManagerInternal.java b/core/java/android/os/UserManagerInternal.java index 26c7a1e2d0ec..898b6cffa9d4 100644 --- a/core/java/android/os/UserManagerInternal.java +++ b/core/java/android/os/UserManagerInternal.java @@ -69,4 +69,16 @@ public abstract class UserManagerInternal { /** Remove a {@link UserRestrictionsListener}. */ public abstract void removeUserRestrictionsListener(UserRestrictionsListener listener); + + /** + * Called by {@link com.android.server.devicepolicy.DevicePolicyManagerService} to update + * whether the device is managed by device owner. + */ + public abstract void setDeviceManaged(boolean isManaged); + + /** + * Called by {@link com.android.server.devicepolicy.DevicePolicyManagerService} to update + * whether the user is managed by profile owner. + */ + public abstract void setUserManaged(int userId, boolean isManaged); } diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java index 4b63c36b8aec..1d4d57278a57 100644 --- a/core/java/android/provider/CallLog.java +++ b/core/java/android/provider/CallLog.java @@ -413,6 +413,14 @@ public class CallLog { public static final String POST_DIAL_DIGITS = "post_dial_digits"; /** + * Indicates that the entry will be copied from primary user to other users. + * <P>Type: INTEGER</P> + * + * @hide + */ + public static final String ADD_FOR_ALL_USERS = "add_for_all_users"; + + /** * If a successful call is made that is longer than this duration, update the phone number * in the ContactsProvider with the normalized version of the number, based on the user's * current country code. @@ -444,7 +452,7 @@ public class CallLog { int presentation, int callType, int features, PhoneAccountHandle accountHandle, long start, int duration, Long dataUsage) { return addCall(ci, context, number, "", presentation, callType, features, accountHandle, - start, duration, dataUsage, false, false); + start, duration, dataUsage, false, null, false); } @@ -467,7 +475,9 @@ public class CallLog { * the call. * @param addForAllUsers If true, the call is added to the call log of all currently * running users. The caller must have the MANAGE_USERS permission if this is true. - * + * @param userToBeInsertedTo {@link UserHandle} of user that the call is going to be + * inserted to. null if it is inserted to the current user. The + * value is ignored if @{link addForAllUsers} is true. * @result The URI of the call log entry belonging to the user that made or received this * call. * {@hide} @@ -475,9 +485,10 @@ public class CallLog { public static Uri addCall(CallerInfo ci, Context context, String number, String postDialDigits, int presentation, int callType, int features, PhoneAccountHandle accountHandle, long start, int duration, Long dataUsage, - boolean addForAllUsers) { + boolean addForAllUsers, UserHandle userToBeInsertedTo) { return addCall(ci, context, number, postDialDigits, presentation, callType, features, - accountHandle, start, duration, dataUsage, addForAllUsers, false); + accountHandle, start, duration, dataUsage, addForAllUsers, userToBeInsertedTo, + false); } /** @@ -501,6 +512,9 @@ public class CallLog { * the call. * @param addForAllUsers If true, the call is added to the call log of all currently * running users. The caller must have the MANAGE_USERS permission if this is true. + * @param userToBeInsertedTo {@link UserHandle} of user that the call is going to be + * inserted to. null if it is inserted to the current user. The + * value is ignored if @{link addForAllUsers} is true. * @param is_read Flag to show if the missed call log has been read by the user or not. * Used for call log restore of missed calls. * @@ -511,7 +525,7 @@ public class CallLog { public static Uri addCall(CallerInfo ci, Context context, String number, String postDialDigits, int presentation, int callType, int features, PhoneAccountHandle accountHandle, long start, int duration, Long dataUsage, - boolean addForAllUsers, boolean is_read) { + boolean addForAllUsers, UserHandle userToBeInsertedTo, boolean is_read) { final ContentResolver resolver = context.getContentResolver(); int numberPresentation = PRESENTATION_ALLOWED; @@ -575,6 +589,7 @@ public class CallLog { values.put(PHONE_ACCOUNT_ID, accountId); values.put(PHONE_ACCOUNT_ADDRESS, accountAddress); values.put(NEW, Integer.valueOf(1)); + values.put(ADD_FOR_ALL_USERS, addForAllUsers ? 1 : 0); if (callType == MISSED_TYPE) { values.put(IS_READ, Integer.valueOf(is_read ? 1 : 0)); @@ -650,9 +665,13 @@ public class CallLog { } } } else { - result = addEntryAndRemoveExpiredEntries(context, CONTENT_URI, values); + Uri uri = CONTENT_URI; + if (userToBeInsertedTo != null) { + uri = ContentProvider + .maybeAddUserId(CONTENT_URI, userToBeInsertedTo.getIdentifier()); + } + result = addEntryAndRemoveExpiredEntries(context, uri, values); } - return result; } diff --git a/core/java/android/security/net/config/NetworkSecurityTrustManager.java b/core/java/android/security/net/config/NetworkSecurityTrustManager.java index 7f5b3ca27bf4..2b860fac45c1 100644 --- a/core/java/android/security/net/config/NetworkSecurityTrustManager.java +++ b/core/java/android/security/net/config/NetworkSecurityTrustManager.java @@ -65,7 +65,7 @@ public class NetworkSecurityTrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - throw new CertificateException("Client authentication not supported"); + mDelegate.checkClientTrusted(chain, authType); } @Override @@ -149,6 +149,6 @@ public class NetworkSecurityTrustManager implements X509TrustManager { @Override public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; + return mDelegate.getAcceptedIssuers(); } } diff --git a/core/java/android/security/net/config/RootTrustManager.java b/core/java/android/security/net/config/RootTrustManager.java index b87bf1fe0695..e307ad00275e 100644 --- a/core/java/android/security/net/config/RootTrustManager.java +++ b/core/java/android/security/net/config/RootTrustManager.java @@ -35,7 +35,6 @@ import javax.net.ssl.X509TrustManager; * @hide */ public class RootTrustManager implements X509TrustManager { private final ApplicationConfig mConfig; - private static final X509Certificate[] EMPTY_ISSUERS = new X509Certificate[0]; public RootTrustManager(ApplicationConfig config) { if (config == null) { @@ -47,7 +46,10 @@ public class RootTrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - throw new CertificateException("Client authentication not supported"); + // Use the default configuration for all client authentication. Domain specific configs are + // only for use in checking server trust not client trust. + NetworkSecurityConfig config = mConfig.getConfigForHostname(""); + config.getTrustManager().checkClientTrusted(chain, authType); } @Override @@ -84,6 +86,10 @@ public class RootTrustManager implements X509TrustManager { @Override public X509Certificate[] getAcceptedIssuers() { - return EMPTY_ISSUERS; + // getAcceptedIssuers is meant to be used to determine which trust anchors the server will + // accept when verifying clients. Domain specific configs are only for use in checking + // server trust not client trust so use the default config. + NetworkSecurityConfig config = mConfig.getConfigForHostname(""); + return config.getTrustManager().getAcceptedIssuers(); } } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 19a98f345498..329d1b06c218 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -436,7 +436,8 @@ public final class InputMethodManager { mCurId = res.id; mBindSequence = res.sequence; } - startInputInner(null, 0, 0, 0); + startInputInner(InputMethodClient.START_INPUT_REASON_BOUND_TO_IMMS, + null, 0, 0, 0); return; } case MSG_UNBIND: { @@ -461,7 +462,9 @@ public final class InputMethodManager { startInput = mActive; } if (startInput) { - startInputInner(null, 0, 0, 0); + startInputInner( + InputMethodClient.START_INPUT_REASON_UNBOUND_FROM_IMMS, null, 0, 0, + 0); } return; } @@ -494,7 +497,10 @@ public final class InputMethodManager { // In that case, we really should not call // mServedInputConnection.finishComposingText. if (checkFocusNoStartInput(mHasBeenInactive, false)) { - startInputInner(null, 0, 0, 0); + final int reason = active ? + InputMethodClient.START_INPUT_REASON_ACTIVATED_BY_IMMS : + InputMethodClient.START_INPUT_REASON_DEACTIVATED_BY_IMMS; + startInputInner(reason, null, 0, 0, 0); } } } @@ -1118,18 +1124,23 @@ public final class InputMethodManager { mServedConnecting = true; } - - startInputInner(null, 0, 0, 0); + + startInputInner(InputMethodClient.START_INPUT_REASON_APP_CALLED_RESTART_INPUT_API, null, 0, + 0, 0); } - - boolean startInputInner(IBinder windowGainingFocus, int controlFlags, int softInputMode, + + boolean startInputInner(@InputMethodClient.StartInputReason final int startInputReason, + IBinder windowGainingFocus, int controlFlags, int softInputMode, int windowFlags) { final View view; synchronized (mH) { view = mServedView; // Make sure we have a window token for the served view. - if (DEBUG) Log.v(TAG, "Starting input: view=" + view); + if (DEBUG) { + Log.v(TAG, "Starting input: view=" + view + + " reason=" + InputMethodClient.getStartInputReason(startInputReason)); + } if (view == null) { if (DEBUG) Log.v(TAG, "ABORT input: no served view!"); return false; @@ -1157,7 +1168,7 @@ public final class InputMethodManager { vh.post(new Runnable() { @Override public void run() { - startInputInner(null, 0, 0, 0); + startInputInner(startInputReason, null, 0, 0, 0); } }); return false; @@ -1221,11 +1232,11 @@ public final class InputMethodManager { + Integer.toHexString(controlFlags)); InputBindResult res; if (windowGainingFocus != null) { - res = mService.windowGainedFocus(mClient, windowGainingFocus, + res = mService.windowGainedFocus(startInputReason, mClient, windowGainingFocus, controlFlags, softInputMode, windowFlags, tba, servedContext); } else { - res = mService.startInput(mClient, + res = mService.startInput(startInputReason, mClient, servedContext, tba, controlFlags); } if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res); @@ -1352,7 +1363,7 @@ public final class InputMethodManager { */ public void checkFocus() { if (checkFocusNoStartInput(false, true)) { - startInputInner(null, 0, 0, 0); + startInputInner(InputMethodClient.START_INPUT_REASON_CHECK_FOCUS, null, 0, 0, 0); } } @@ -1440,8 +1451,8 @@ public final class InputMethodManager { // should be done in conjunction with telling the system service // about the window gaining focus, to help make the transition // smooth. - if (startInputInner(rootView.getWindowToken(), - controlFlags, softInputMode, windowFlags)) { + if (startInputInner(InputMethodClient.START_INPUT_REASON_WINDOW_FOCUS_GAIN, + rootView.getWindowToken(), controlFlags, softInputMode, windowFlags)) { return; } } @@ -1451,8 +1462,10 @@ public final class InputMethodManager { synchronized (mH) { try { if (DEBUG) Log.v(TAG, "Reporting focus gain, without startInput"); - mService.windowGainedFocus(mClient, rootView.getWindowToken(), - controlFlags, softInputMode, windowFlags, null, null); + mService.windowGainedFocus( + InputMethodClient.START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient, + rootView.getWindowToken(), controlFlags, softInputMode, windowFlags, null, + null); } catch (RemoteException e) { } } diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 60c5e420d24f..db3ecc6f2dc8 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -45,9 +45,10 @@ interface IInputMethodManager { void addClient(in IInputMethodClient client, in IInputContext inputContext, int uid, int pid); void removeClient(in IInputMethodClient client); - - InputBindResult startInput(in IInputMethodClient client, - IInputContext inputContext, in EditorInfo attribute, int controlFlags); + + InputBindResult startInput(/* @InputMethodClient.StartInputReason */ int startInputReason, + in IInputMethodClient client, IInputContext inputContext, in EditorInfo attribute, + int controlFlags); void finishInput(in IInputMethodClient client); boolean showSoftInput(in IInputMethodClient client, int flags, in ResultReceiver resultReceiver); @@ -55,9 +56,11 @@ interface IInputMethodManager { in ResultReceiver resultReceiver); // Report that a window has gained focus. If 'attribute' is non-null, // this will also do a startInput. - InputBindResult windowGainedFocus(in IInputMethodClient client, in IBinder windowToken, - int controlFlags, int softInputMode, int windowFlags, - in EditorInfo attribute, IInputContext inputContext); + InputBindResult windowGainedFocus( + /* @InputMethodClient.StartInputReason */ int startInputReason, + in IInputMethodClient client, in IBinder windowToken, int controlFlags, + int softInputMode, int windowFlags, in EditorInfo attribute, + IInputContext inputContext); void showInputMethodPickerFromClient(in IInputMethodClient client, int auxiliarySubtypeMode); diff --git a/core/java/com/android/internal/view/InputMethodClient.java b/core/java/com/android/internal/view/InputMethodClient.java index a0353434adf9..c27e9aa3f2bb 100644 --- a/core/java/com/android/internal/view/InputMethodClient.java +++ b/core/java/com/android/internal/view/InputMethodClient.java @@ -23,6 +23,49 @@ import java.lang.annotation.Retention; import static java.lang.annotation.RetentionPolicy.SOURCE; public final class InputMethodClient { + public static final int START_INPUT_REASON_UNSPECIFIED = 0; + public static final int START_INPUT_REASON_WINDOW_FOCUS_GAIN = 1; + public static final int START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY = 2; + public static final int START_INPUT_REASON_APP_CALLED_RESTART_INPUT_API = 3; + public static final int START_INPUT_REASON_CHECK_FOCUS = 4; + public static final int START_INPUT_REASON_BOUND_TO_IMMS = 5; + public static final int START_INPUT_REASON_UNBOUND_FROM_IMMS = 6; + public static final int START_INPUT_REASON_ACTIVATED_BY_IMMS = 7; + public static final int START_INPUT_REASON_DEACTIVATED_BY_IMMS = 8; + + @Retention(SOURCE) + @IntDef({START_INPUT_REASON_UNSPECIFIED, START_INPUT_REASON_WINDOW_FOCUS_GAIN, + START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY, + START_INPUT_REASON_APP_CALLED_RESTART_INPUT_API, START_INPUT_REASON_CHECK_FOCUS, + START_INPUT_REASON_BOUND_TO_IMMS, START_INPUT_REASON_ACTIVATED_BY_IMMS, + START_INPUT_REASON_DEACTIVATED_BY_IMMS}) + public @interface StartInputReason {} + + public static String getStartInputReason(@StartInputReason final int reason) { + switch (reason) { + case START_INPUT_REASON_UNSPECIFIED: + return "UNSPECIFIED"; + case START_INPUT_REASON_WINDOW_FOCUS_GAIN: + return "WINDOW_FOCUS_GAIN"; + case START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY: + return "WINDOW_FOCUS_GAIN_REPORT_ONLY"; + case START_INPUT_REASON_APP_CALLED_RESTART_INPUT_API: + return "APP_CALLED_RESTART_INPUT_API"; + case START_INPUT_REASON_CHECK_FOCUS: + return "CHECK_FOCUS"; + case START_INPUT_REASON_BOUND_TO_IMMS: + return "BOUND_TO_IMMS"; + case START_INPUT_REASON_UNBOUND_FROM_IMMS: + return "UNBOUND_FROM_IMMS"; + case START_INPUT_REASON_ACTIVATED_BY_IMMS: + return "ACTIVATED_BY_IMMS"; + case START_INPUT_REASON_DEACTIVATED_BY_IMMS: + return "DEACTIVATED_BY_IMMS"; + default: + return "Unknown=" + reason; + } + } + public static final int UNBIND_REASON_UNSPECIFIED = 0; public static final int UNBIND_REASON_SWITCH_CLIENT = 1; public static final int UNBIND_REASON_SWITCH_IME = 2; diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk index cc68fb292a8f..fc4916c79d09 100644 --- a/libs/hwui/Android.mk +++ b/libs/hwui/Android.mk @@ -101,6 +101,7 @@ hwui_cflags += -Wno-free-nonheap-object ifeq (true, $(HWUI_NEW_OPS)) hwui_src_files += \ + BakedOpDispatcher.cpp \ BakedOpRenderer.cpp \ OpReorderer.cpp \ RecordingCanvas.cpp @@ -275,7 +276,9 @@ LOCAL_MULTILIB := both LOCAL_MODULE_STEM_32 := hwuimicro LOCAL_MODULE_STEM_64 := hwuimicro64 LOCAL_SHARED_LIBRARIES := $(hwui_shared_libraries) -LOCAL_CFLAGS := $(hwui_cflags) +LOCAL_CFLAGS := \ + $(hwui_cflags) \ + -DHWUI_NULL_GPU LOCAL_C_INCLUDES += bionic/benchmarks/ LOCAL_WHOLE_STATIC_LIBRARIES := libhwui_static_null_gpu diff --git a/libs/hwui/BakedOpDispatcher.cpp b/libs/hwui/BakedOpDispatcher.cpp new file mode 100644 index 000000000000..b56b1e4c5f93 --- /dev/null +++ b/libs/hwui/BakedOpDispatcher.cpp @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BakedOpDispatcher.h" + +#include "BakedOpRenderer.h" +#include "Caches.h" +#include "Glop.h" +#include "GlopBuilder.h" +#include "renderstate/OffscreenBufferPool.h" +#include "renderstate/RenderState.h" +#include "utils/GLUtils.h" +#include "VertexBuffer.h" + +#include <algorithm> +#include <math.h> + +namespace android { +namespace uirenderer { + +void BakedOpDispatcher::onRenderNodeOp(BakedOpRenderer&, const RenderNodeOp&, const BakedOpState&) { + LOG_ALWAYS_FATAL("unsupported operation"); +} + +void BakedOpDispatcher::onBeginLayerOp(BakedOpRenderer& renderer, const BeginLayerOp& op, const BakedOpState& state) { + LOG_ALWAYS_FATAL("unsupported operation"); +} + +void BakedOpDispatcher::onEndLayerOp(BakedOpRenderer& renderer, const EndLayerOp& op, const BakedOpState& state) { + LOG_ALWAYS_FATAL("unsupported operation"); +} + +void BakedOpDispatcher::onBitmapOp(BakedOpRenderer& renderer, const BitmapOp& op, const BakedOpState& state) { + renderer.caches().textureState().activateTexture(0); // TODO: should this be automatic, and/or elsewhere? + Texture* texture = renderer.getTexture(op.bitmap); + if (!texture) return; + const AutoTexture autoCleanup(texture); + + const int textureFillFlags = (op.bitmap->colorType() == kAlpha_8_SkColorType) + ? TextureFillFlags::IsAlphaMaskTexture : TextureFillFlags::None; + Glop glop; + GlopBuilder(renderer.renderState(), renderer.caches(), &glop) + .setRoundRectClipState(state.roundRectClipState) + .setMeshTexturedUnitQuad(texture->uvMapper) + .setFillTexturePaint(*texture, textureFillFlags, op.paint, state.alpha) + .setTransform(state.computedState.transform, TransformFlags::None) + .setModelViewMapUnitToRectSnap(Rect(0, 0, texture->width, texture->height)) + .build(); + renderer.renderGlop(state, glop); +} + +void BakedOpDispatcher::onLinesOp(BakedOpRenderer& renderer, const LinesOp& op, const BakedOpState& state) { + LOG_ALWAYS_FATAL("todo"); +} + +void BakedOpDispatcher::onRectOp(BakedOpRenderer& renderer, const RectOp& op, const BakedOpState& state) { + Glop glop; + GlopBuilder(renderer.renderState(), renderer.caches(), &glop) + .setRoundRectClipState(state.roundRectClipState) + .setMeshUnitQuad() + .setFillPaint(*op.paint, state.alpha) + .setTransform(state.computedState.transform, TransformFlags::None) + .setModelViewMapUnitToRect(op.unmappedBounds) + .build(); + renderer.renderGlop(state, glop); +} + +namespace VertexBufferRenderFlags { + enum { + Offset = 0x1, + ShadowInterp = 0x2, + }; +} + +static void renderVertexBuffer(BakedOpRenderer& renderer, const BakedOpState& state, + const VertexBuffer& vertexBuffer, float translateX, float translateY, + SkPaint& paint, int vertexBufferRenderFlags) { + if (CC_LIKELY(vertexBuffer.getVertexCount())) { + bool shadowInterp = vertexBufferRenderFlags & VertexBufferRenderFlags::ShadowInterp; + const int transformFlags = TransformFlags::OffsetByFudgeFactor; + Glop glop; + GlopBuilder(renderer.renderState(), renderer.caches(), &glop) + .setRoundRectClipState(state.roundRectClipState) + .setMeshVertexBuffer(vertexBuffer, shadowInterp) + .setFillPaint(paint, state.alpha) + .setTransform(state.computedState.transform, transformFlags) + .setModelViewOffsetRect(translateX, translateY, vertexBuffer.getBounds()) + .build(); + renderer.renderGlop(state, glop); + } +} + +static void renderShadow(BakedOpRenderer& renderer, const BakedOpState& state, float casterAlpha, + const VertexBuffer* ambientShadowVertexBuffer, const VertexBuffer* spotShadowVertexBuffer) { + SkPaint paint; + paint.setAntiAlias(true); // want to use AlphaVertex + + // The caller has made sure casterAlpha > 0. + uint8_t ambientShadowAlpha = renderer.getLightInfo().ambientShadowAlpha; + if (CC_UNLIKELY(Properties::overrideAmbientShadowStrength >= 0)) { + ambientShadowAlpha = Properties::overrideAmbientShadowStrength; + } + if (ambientShadowVertexBuffer && ambientShadowAlpha > 0) { + paint.setAlpha((uint8_t)(casterAlpha * ambientShadowAlpha)); + renderVertexBuffer(renderer, state, *ambientShadowVertexBuffer, 0, 0, + paint, VertexBufferRenderFlags::ShadowInterp); + } + + uint8_t spotShadowAlpha = renderer.getLightInfo().spotShadowAlpha; + if (CC_UNLIKELY(Properties::overrideSpotShadowStrength >= 0)) { + spotShadowAlpha = Properties::overrideSpotShadowStrength; + } + if (spotShadowVertexBuffer && spotShadowAlpha > 0) { + paint.setAlpha((uint8_t)(casterAlpha * spotShadowAlpha)); + renderVertexBuffer(renderer, state, *spotShadowVertexBuffer, 0, 0, + paint, VertexBufferRenderFlags::ShadowInterp); + } +} + +void BakedOpDispatcher::onShadowOp(BakedOpRenderer& renderer, const ShadowOp& op, const BakedOpState& state) { + TessellationCache::vertexBuffer_pair_t buffers; + renderer.caches().tessellationCache.getShadowBuffers(&state.computedState.transform, + op.localClipRect, op.casterAlpha >= 1.0f, op.casterPath, + &op.shadowMatrixXY, &op.shadowMatrixZ, + op.lightCenter, renderer.getLightInfo().lightRadius, + buffers); + + renderShadow(renderer, state, op.casterAlpha, buffers.first, buffers.second); +} + +void BakedOpDispatcher::onSimpleRectsOp(BakedOpRenderer& renderer, const SimpleRectsOp& op, const BakedOpState& state) { + Glop glop; + GlopBuilder(renderer.renderState(), renderer.caches(), &glop) + .setRoundRectClipState(state.roundRectClipState) + .setMeshIndexedQuads(&op.vertices[0], op.vertexCount / 4) + .setFillPaint(*op.paint, state.alpha) + .setTransform(state.computedState.transform, TransformFlags::None) + .setModelViewOffsetRect(0, 0, op.unmappedBounds) + .build(); + renderer.renderGlop(state, glop); +} + +static void renderTextShadow(BakedOpRenderer& renderer, FontRenderer& fontRenderer, + const TextOp& op, const BakedOpState& state) { + renderer.caches().textureState().activateTexture(0); + + PaintUtils::TextShadow textShadow; + if (!PaintUtils::getTextShadow(op.paint, &textShadow)) { + LOG_ALWAYS_FATAL("failed to query shadow attributes"); + } + + renderer.caches().dropShadowCache.setFontRenderer(fontRenderer); + ShadowTexture* texture = renderer.caches().dropShadowCache.get( + op.paint, (const char*) op.glyphs, + op.glyphCount, textShadow.radius, op.positions); + // If the drop shadow exceeds the max texture size or couldn't be + // allocated, skip drawing + if (!texture) return; + const AutoTexture autoCleanup(texture); + + const float sx = op.x - texture->left + textShadow.dx; + const float sy = op.y - texture->top + textShadow.dy; + + Glop glop; + GlopBuilder(renderer.renderState(), renderer.caches(), &glop) + .setRoundRectClipState(state.roundRectClipState) + .setMeshTexturedUnitQuad(nullptr) + .setFillShadowTexturePaint(*texture, textShadow.color, *op.paint, state.alpha) + .setTransform(state.computedState.transform, TransformFlags::None) + .setModelViewMapUnitToRect(Rect(sx, sy, sx + texture->width, sy + texture->height)) + .build(); + renderer.renderGlop(state, glop); +} + +void BakedOpDispatcher::onTextOp(BakedOpRenderer& renderer, const TextOp& op, const BakedOpState& state) { + FontRenderer& fontRenderer = renderer.caches().fontRenderer.getFontRenderer(); + + if (CC_UNLIKELY(PaintUtils::hasTextShadow(op.paint))) { + fontRenderer.setFont(op.paint, SkMatrix::I()); + renderTextShadow(renderer, fontRenderer, op, state); + } + + float x = op.x; + float y = op.y; + const Matrix4& transform = state.computedState.transform; + const bool pureTranslate = transform.isPureTranslate(); + if (CC_LIKELY(pureTranslate)) { + x = floorf(x + transform.getTranslateX() + 0.5f); + y = floorf(y + transform.getTranslateY() + 0.5f); + fontRenderer.setFont(op.paint, SkMatrix::I()); + fontRenderer.setTextureFiltering(false); + } else if (CC_UNLIKELY(transform.isPerspective())) { + fontRenderer.setFont(op.paint, SkMatrix::I()); + fontRenderer.setTextureFiltering(true); + } else { + // We only pass a partial transform to the font renderer. That partial + // matrix defines how glyphs are rasterized. Typically we want glyphs + // to be rasterized at their final size on screen, which means the partial + // matrix needs to take the scale factor into account. + // When a partial matrix is used to transform glyphs during rasterization, + // the mesh is generated with the inverse transform (in the case of scale, + // the mesh is generated at 1.0 / scale for instance.) This allows us to + // apply the full transform matrix at draw time in the vertex shader. + // Applying the full matrix in the shader is the easiest way to handle + // rotation and perspective and allows us to always generated quads in the + // font renderer which greatly simplifies the code, clipping in particular. + float sx, sy; + transform.decomposeScale(sx, sy); + fontRenderer.setFont(op.paint, SkMatrix::MakeScale( + roundf(std::max(1.0f, sx)), + roundf(std::max(1.0f, sy)))); + fontRenderer.setTextureFiltering(true); + } + + // TODO: Implement better clipping for scaled/rotated text + const Rect* clip = !pureTranslate ? nullptr : &state.computedState.clipRect; + Rect layerBounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f); + + int alpha = PaintUtils::getAlphaDirect(op.paint) * state.alpha; + SkXfermode::Mode mode = PaintUtils::getXfermodeDirect(op.paint); + TextDrawFunctor functor(&renderer, &state, x, y, pureTranslate, alpha, mode, op.paint); + + bool hasActiveLayer = false; // TODO + fontRenderer.renderPosText(op.paint, clip, (const char*) op.glyphs, op.glyphCount, x, y, + op.positions, hasActiveLayer ? &layerBounds : nullptr, &functor, true); // TODO: merging +} + +void BakedOpDispatcher::onLayerOp(BakedOpRenderer& renderer, const LayerOp& op, const BakedOpState& state) { + OffscreenBuffer* buffer = *op.layerHandle; + + // TODO: extend this to handle HW layers & paint properties which + // reside in node.properties().layerProperties() + float layerAlpha = op.alpha * state.alpha; + Glop glop; + GlopBuilder(renderer.renderState(), renderer.caches(), &glop) + .setRoundRectClipState(state.roundRectClipState) + .setMeshTexturedIndexedVbo(buffer->vbo, buffer->elementCount) + .setFillLayer(buffer->texture, op.colorFilter, layerAlpha, op.mode, Blend::ModeOrderSwap::NoSwap) + .setTransform(state.computedState.transform, TransformFlags::None) + .setModelViewOffsetRectSnap(op.unmappedBounds.left, op.unmappedBounds.top, + Rect(op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight())) + .build(); + renderer.renderGlop(state, glop); + + if (op.destroy) { + renderer.renderState().layerPool().putOrDelete(buffer); + } +} + +} // namespace uirenderer +} // namespace android diff --git a/libs/hwui/BakedOpDispatcher.h b/libs/hwui/BakedOpDispatcher.h new file mode 100644 index 000000000000..caf14bfeef6d --- /dev/null +++ b/libs/hwui/BakedOpDispatcher.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_HWUI_BAKED_OP_DISPATCHER_H +#define ANDROID_HWUI_BAKED_OP_DISPATCHER_H + +#include "BakedOpState.h" +#include "RecordedOp.h" + +namespace android { +namespace uirenderer { + +/** + * Provides all "onBitmapOp(...)" style static methods for every op type, which convert the + * RecordedOps and their state to Glops, and renders them with the provided BakedOpRenderer. + * + * This dispatcher is separate from the renderer so that the dispatcher / renderer interaction is + * minimal through public BakedOpRenderer APIs. + */ +class BakedOpDispatcher { +public: + // Declares all "onBitmapOp(...)" style methods for every op type +#define DISPATCH_METHOD(Type) \ + static void on##Type(BakedOpRenderer& renderer, const Type& op, const BakedOpState& state); + MAP_OPS(DISPATCH_METHOD); +}; + +}; // namespace uirenderer +}; // namespace android + +#endif // ANDROID_HWUI_BAKED_OP_DISPATCHER_H diff --git a/libs/hwui/BakedOpRenderer.cpp b/libs/hwui/BakedOpRenderer.cpp index d13d7ef81e5b..6cdc320a0bad 100644 --- a/libs/hwui/BakedOpRenderer.cpp +++ b/libs/hwui/BakedOpRenderer.cpp @@ -25,15 +25,10 @@ #include "VertexBuffer.h" #include <algorithm> -#include <math.h> namespace android { namespace uirenderer { -//////////////////////////////////////////////////////////////////////////////// -// BakedOpRenderer -//////////////////////////////////////////////////////////////////////////////// - OffscreenBuffer* BakedOpRenderer::startTemporaryLayer(uint32_t width, uint32_t height) { LOG_ALWAYS_FATAL_IF(mRenderTarget.offscreenBuffer, "already has layer..."); @@ -151,238 +146,5 @@ void BakedOpRenderer::renderGlop(const BakedOpState& state, const Glop& glop) { if (!mRenderTarget.frameBufferId) mHasDrawn = true; } -//////////////////////////////////////////////////////////////////////////////// -// static BakedOpDispatcher methods -//////////////////////////////////////////////////////////////////////////////// - -void BakedOpDispatcher::onRenderNodeOp(BakedOpRenderer&, const RenderNodeOp&, const BakedOpState&) { - LOG_ALWAYS_FATAL("unsupported operation"); -} - -void BakedOpDispatcher::onBeginLayerOp(BakedOpRenderer& renderer, const BeginLayerOp& op, const BakedOpState& state) { - LOG_ALWAYS_FATAL("unsupported operation"); -} - -void BakedOpDispatcher::onEndLayerOp(BakedOpRenderer& renderer, const EndLayerOp& op, const BakedOpState& state) { - LOG_ALWAYS_FATAL("unsupported operation"); -} - -void BakedOpDispatcher::onBitmapOp(BakedOpRenderer& renderer, const BitmapOp& op, const BakedOpState& state) { - renderer.caches().textureState().activateTexture(0); // TODO: should this be automatic, and/or elsewhere? - Texture* texture = renderer.getTexture(op.bitmap); - if (!texture) return; - const AutoTexture autoCleanup(texture); - - const int textureFillFlags = (op.bitmap->colorType() == kAlpha_8_SkColorType) - ? TextureFillFlags::IsAlphaMaskTexture : TextureFillFlags::None; - Glop glop; - GlopBuilder(renderer.renderState(), renderer.caches(), &glop) - .setRoundRectClipState(state.roundRectClipState) - .setMeshTexturedUnitQuad(texture->uvMapper) - .setFillTexturePaint(*texture, textureFillFlags, op.paint, state.alpha) - .setTransform(state.computedState.transform, TransformFlags::None) - .setModelViewMapUnitToRectSnap(Rect(0, 0, texture->width, texture->height)) - .build(); - renderer.renderGlop(state, glop); -} - -void BakedOpDispatcher::onLinesOp(BakedOpRenderer& renderer, const LinesOp& op, const BakedOpState& state) { - LOG_ALWAYS_FATAL("todo"); -} - -void BakedOpDispatcher::onRectOp(BakedOpRenderer& renderer, const RectOp& op, const BakedOpState& state) { - Glop glop; - GlopBuilder(renderer.renderState(), renderer.caches(), &glop) - .setRoundRectClipState(state.roundRectClipState) - .setMeshUnitQuad() - .setFillPaint(*op.paint, state.alpha) - .setTransform(state.computedState.transform, TransformFlags::None) - .setModelViewMapUnitToRect(op.unmappedBounds) - .build(); - renderer.renderGlop(state, glop); -} - -namespace VertexBufferRenderFlags { - enum { - Offset = 0x1, - ShadowInterp = 0x2, - }; -} - -static void renderVertexBuffer(BakedOpRenderer& renderer, const BakedOpState& state, - const VertexBuffer& vertexBuffer, float translateX, float translateY, - SkPaint& paint, int vertexBufferRenderFlags) { - if (CC_LIKELY(vertexBuffer.getVertexCount())) { - bool shadowInterp = vertexBufferRenderFlags & VertexBufferRenderFlags::ShadowInterp; - const int transformFlags = TransformFlags::OffsetByFudgeFactor; - Glop glop; - GlopBuilder(renderer.renderState(), renderer.caches(), &glop) - .setRoundRectClipState(state.roundRectClipState) - .setMeshVertexBuffer(vertexBuffer, shadowInterp) - .setFillPaint(paint, state.alpha) - .setTransform(state.computedState.transform, transformFlags) - .setModelViewOffsetRect(translateX, translateY, vertexBuffer.getBounds()) - .build(); - renderer.renderGlop(state, glop); - } -} - -static void renderShadow(BakedOpRenderer& renderer, const BakedOpState& state, float casterAlpha, - const VertexBuffer* ambientShadowVertexBuffer, const VertexBuffer* spotShadowVertexBuffer) { - SkPaint paint; - paint.setAntiAlias(true); // want to use AlphaVertex - - // The caller has made sure casterAlpha > 0. - uint8_t ambientShadowAlpha = renderer.getLightInfo().ambientShadowAlpha; - if (CC_UNLIKELY(Properties::overrideAmbientShadowStrength >= 0)) { - ambientShadowAlpha = Properties::overrideAmbientShadowStrength; - } - if (ambientShadowVertexBuffer && ambientShadowAlpha > 0) { - paint.setAlpha((uint8_t)(casterAlpha * ambientShadowAlpha)); - renderVertexBuffer(renderer, state, *ambientShadowVertexBuffer, 0, 0, - paint, VertexBufferRenderFlags::ShadowInterp); - } - - uint8_t spotShadowAlpha = renderer.getLightInfo().spotShadowAlpha; - if (CC_UNLIKELY(Properties::overrideSpotShadowStrength >= 0)) { - spotShadowAlpha = Properties::overrideSpotShadowStrength; - } - if (spotShadowVertexBuffer && spotShadowAlpha > 0) { - paint.setAlpha((uint8_t)(casterAlpha * spotShadowAlpha)); - renderVertexBuffer(renderer, state, *spotShadowVertexBuffer, 0, 0, - paint, VertexBufferRenderFlags::ShadowInterp); - } -} - -void BakedOpDispatcher::onShadowOp(BakedOpRenderer& renderer, const ShadowOp& op, const BakedOpState& state) { - TessellationCache::vertexBuffer_pair_t buffers; - renderer.caches().tessellationCache.getShadowBuffers(&state.computedState.transform, - op.localClipRect, op.casterAlpha >= 1.0f, op.casterPath, - &op.shadowMatrixXY, &op.shadowMatrixZ, - op.lightCenter, renderer.getLightInfo().lightRadius, - buffers); - - renderShadow(renderer, state, op.casterAlpha, buffers.first, buffers.second); -} - -void BakedOpDispatcher::onSimpleRectsOp(BakedOpRenderer& renderer, const SimpleRectsOp& op, const BakedOpState& state) { - Glop glop; - GlopBuilder(renderer.renderState(), renderer.caches(), &glop) - .setRoundRectClipState(state.roundRectClipState) - .setMeshIndexedQuads(&op.vertices[0], op.vertexCount / 4) - .setFillPaint(*op.paint, state.alpha) - .setTransform(state.computedState.transform, TransformFlags::None) - .setModelViewOffsetRect(0, 0, op.unmappedBounds) - .build(); - renderer.renderGlop(state, glop); -} - -static void renderTextShadow(BakedOpRenderer& renderer, FontRenderer& fontRenderer, - const TextOp& op, const BakedOpState& state) { - renderer.caches().textureState().activateTexture(0); - - PaintUtils::TextShadow textShadow; - if (!PaintUtils::getTextShadow(op.paint, &textShadow)) { - LOG_ALWAYS_FATAL("failed to query shadow attributes"); - } - - renderer.caches().dropShadowCache.setFontRenderer(fontRenderer); - ShadowTexture* texture = renderer.caches().dropShadowCache.get( - op.paint, (const char*) op.glyphs, - op.glyphCount, textShadow.radius, op.positions); - // If the drop shadow exceeds the max texture size or couldn't be - // allocated, skip drawing - if (!texture) return; - const AutoTexture autoCleanup(texture); - - const float sx = op.x - texture->left + textShadow.dx; - const float sy = op.y - texture->top + textShadow.dy; - - Glop glop; - GlopBuilder(renderer.renderState(), renderer.caches(), &glop) - .setRoundRectClipState(state.roundRectClipState) - .setMeshTexturedUnitQuad(nullptr) - .setFillShadowTexturePaint(*texture, textShadow.color, *op.paint, state.alpha) - .setTransform(state.computedState.transform, TransformFlags::None) - .setModelViewMapUnitToRect(Rect(sx, sy, sx + texture->width, sy + texture->height)) - .build(); - renderer.renderGlop(state, glop); -} - -void BakedOpDispatcher::onTextOp(BakedOpRenderer& renderer, const TextOp& op, const BakedOpState& state) { - FontRenderer& fontRenderer = renderer.caches().fontRenderer.getFontRenderer(); - - if (CC_UNLIKELY(PaintUtils::hasTextShadow(op.paint))) { - fontRenderer.setFont(op.paint, SkMatrix::I()); - renderTextShadow(renderer, fontRenderer, op, state); - } - - float x = op.x; - float y = op.y; - const Matrix4& transform = state.computedState.transform; - const bool pureTranslate = transform.isPureTranslate(); - if (CC_LIKELY(pureTranslate)) { - x = floorf(x + transform.getTranslateX() + 0.5f); - y = floorf(y + transform.getTranslateY() + 0.5f); - fontRenderer.setFont(op.paint, SkMatrix::I()); - fontRenderer.setTextureFiltering(false); - } else if (CC_UNLIKELY(transform.isPerspective())) { - fontRenderer.setFont(op.paint, SkMatrix::I()); - fontRenderer.setTextureFiltering(true); - } else { - // We only pass a partial transform to the font renderer. That partial - // matrix defines how glyphs are rasterized. Typically we want glyphs - // to be rasterized at their final size on screen, which means the partial - // matrix needs to take the scale factor into account. - // When a partial matrix is used to transform glyphs during rasterization, - // the mesh is generated with the inverse transform (in the case of scale, - // the mesh is generated at 1.0 / scale for instance.) This allows us to - // apply the full transform matrix at draw time in the vertex shader. - // Applying the full matrix in the shader is the easiest way to handle - // rotation and perspective and allows us to always generated quads in the - // font renderer which greatly simplifies the code, clipping in particular. - float sx, sy; - transform.decomposeScale(sx, sy); - fontRenderer.setFont(op.paint, SkMatrix::MakeScale( - roundf(std::max(1.0f, sx)), - roundf(std::max(1.0f, sy)))); - fontRenderer.setTextureFiltering(true); - } - - // TODO: Implement better clipping for scaled/rotated text - const Rect* clip = !pureTranslate ? nullptr : &state.computedState.clipRect; - Rect layerBounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f); - - int alpha = PaintUtils::getAlphaDirect(op.paint) * state.alpha; - SkXfermode::Mode mode = PaintUtils::getXfermodeDirect(op.paint); - TextDrawFunctor functor(&renderer, &state, x, y, pureTranslate, alpha, mode, op.paint); - - bool hasActiveLayer = false; // TODO - fontRenderer.renderPosText(op.paint, clip, (const char*) op.glyphs, op.glyphCount, x, y, - op.positions, hasActiveLayer ? &layerBounds : nullptr, &functor, true); // TODO: merging -} - -void BakedOpDispatcher::onLayerOp(BakedOpRenderer& renderer, const LayerOp& op, const BakedOpState& state) { - OffscreenBuffer* buffer = *op.layerHandle; - - // TODO: extend this to handle HW layers & paint properties which - // reside in node.properties().layerProperties() - float layerAlpha = op.alpha * state.alpha; - Glop glop; - GlopBuilder(renderer.renderState(), renderer.caches(), &glop) - .setRoundRectClipState(state.roundRectClipState) - .setMeshTexturedIndexedVbo(buffer->vbo, buffer->elementCount) - .setFillLayer(buffer->texture, op.colorFilter, layerAlpha, op.mode, Blend::ModeOrderSwap::NoSwap) - .setTransform(state.computedState.transform, TransformFlags::None) - .setModelViewOffsetRectSnap(op.unmappedBounds.left, op.unmappedBounds.top, - Rect(op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight())) - .build(); - renderer.renderGlop(state, glop); - - if (op.destroy) { - renderer.renderState().layerPool().putOrDelete(buffer); - } -} - } // namespace uirenderer } // namespace android diff --git a/libs/hwui/BakedOpRenderer.h b/libs/hwui/BakedOpRenderer.h index 29f9a6f81242..62d183846e27 100644 --- a/libs/hwui/BakedOpRenderer.h +++ b/libs/hwui/BakedOpRenderer.h @@ -91,21 +91,6 @@ private: const LightInfo mLightInfo; }; -/** - * Provides all "onBitmapOp(...)" style static methods for every op type, which convert the - * RecordedOps and their state to Glops, and renders them with the provided BakedOpRenderer. - * - * This dispatcher is separate from the renderer so that the dispatcher / renderer interaction is - * minimal through public BakedOpRenderer APIs. - */ -class BakedOpDispatcher { -public: - // Declares all "onBitmapOp(...)" style methods for every op type -#define DISPATCH_METHOD(Type) \ - static void on##Type(BakedOpRenderer& renderer, const Type& op, const BakedOpState& state); - MAP_OPS(DISPATCH_METHOD); -}; - }; // namespace uirenderer }; // namespace android diff --git a/libs/hwui/DisplayList.h b/libs/hwui/DisplayList.h index 00c4e2d47e4c..60cc7bab64dd 100644 --- a/libs/hwui/DisplayList.h +++ b/libs/hwui/DisplayList.h @@ -132,7 +132,7 @@ public: DisplayList(); ~DisplayList(); - // index of DisplayListOp restore, after which projected descendents should be drawn + // index of DisplayListOp restore, after which projected descendants should be drawn int projectionReceiveIndex; const LsaVector<Chunk>& getChunks() const { return chunks; } diff --git a/libs/hwui/DisplayListCanvas.h b/libs/hwui/DisplayListCanvas.h index bf98f79d4d5a..72fc100ebd1d 100644 --- a/libs/hwui/DisplayListCanvas.h +++ b/libs/hwui/DisplayListCanvas.h @@ -55,6 +55,7 @@ class DeferredDisplayList; class DeferredLayerUpdater; class DisplayListOp; class DrawOp; +class DrawRenderNodeOp; class RenderNode; class StateOp; diff --git a/libs/hwui/DisplayListOp.h b/libs/hwui/DisplayListOp.h index 977b53c31f46..bd11d0acf176 100644 --- a/libs/hwui/DisplayListOp.h +++ b/libs/hwui/DisplayListOp.h @@ -1386,19 +1386,19 @@ public: : DrawBoundedOp(0, 0, renderNode->getWidth(), renderNode->getHeight(), nullptr) , renderNode(renderNode) , mRecordedWithPotentialStencilClip(!clipIsSimple || !transformFromParent.isSimple()) - , mTransformFromParent(transformFromParent) - , mSkipInOrderDraw(false) {} + , localMatrix(transformFromParent) + , skipInOrderDraw(false) {} virtual void defer(DeferStateStruct& deferStruct, int saveCount, int level, bool useQuickReject) override { - if (renderNode->isRenderable() && !mSkipInOrderDraw) { + if (renderNode->isRenderable() && !skipInOrderDraw) { renderNode->defer(deferStruct, level + 1); } } virtual void replay(ReplayStateStruct& replayStruct, int saveCount, int level, bool useQuickReject) override { - if (renderNode->isRenderable() && !mSkipInOrderDraw) { + if (renderNode->isRenderable() && !skipInOrderDraw) { renderNode->replay(replayStruct, level + 1); } } @@ -1439,7 +1439,7 @@ private: /** * Records transform vs parent, used for computing total transform without rerunning DL contents */ - const mat4 mTransformFromParent; + const mat4 localMatrix; /** * Holds the transformation between the projection surface ViewGroup and this RenderNode @@ -1449,8 +1449,8 @@ private: * * Note: doesn't include transformation within the RenderNode, or its properties. */ - mat4 mTransformFromCompositingAncestor; - bool mSkipInOrderDraw; + mat4 transformFromCompositingAncestor; + bool skipInOrderDraw; }; /** diff --git a/libs/hwui/FontRenderer.cpp b/libs/hwui/FontRenderer.cpp index 5f33cae2f91a..47654f400ec2 100644 --- a/libs/hwui/FontRenderer.cpp +++ b/libs/hwui/FontRenderer.cpp @@ -29,8 +29,9 @@ #if HWUI_NEW_OPS -#include "BakedOpState.h" +#include "BakedOpDispatcher.h" #include "BakedOpRenderer.h" +#include "BakedOpState.h" #else #include "OpenGLRenderer.h" #endif diff --git a/libs/hwui/OpReorderer.cpp b/libs/hwui/OpReorderer.cpp index 5e954ae6e971..9cbd9c2d9ffc 100644 --- a/libs/hwui/OpReorderer.cpp +++ b/libs/hwui/OpReorderer.cpp @@ -330,6 +330,7 @@ OpReorderer::OpReorderer(const LayerUpdateQueue& layers, const SkRect& clip, for (int i = layers.entries().size() - 1; i >= 0; i--) { RenderNode* layerNode = layers.entries()[i].renderNode; const Rect& layerDamage = layers.entries()[i].damage; + layerNode->computeOrdering(); // map current light center into RenderNode's coordinate space Vector3 lightCenter = mCanvasState.currentSnapshot()->getRelativeLightCenter(); @@ -339,7 +340,7 @@ OpReorderer::OpReorderer(const LayerUpdateQueue& layers, const SkRect& clip, layerDamage, lightCenter, nullptr, layerNode); if (layerNode->getDisplayList()) { - deferDisplayList(*(layerNode->getDisplayList())); + deferNodeOps(*layerNode); } restoreForLayer(); } @@ -347,6 +348,7 @@ OpReorderer::OpReorderer(const LayerUpdateQueue& layers, const SkRect& clip, // Defer Fbo0 for (const sp<RenderNode>& node : nodes) { if (node->nothingToDraw()) continue; + node->computeOrdering(); int count = mCanvasState.save(SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag); deferNodePropsAndOps(*node); @@ -354,20 +356,6 @@ OpReorderer::OpReorderer(const LayerUpdateQueue& layers, const SkRect& clip, } } -OpReorderer::OpReorderer(int viewportWidth, int viewportHeight, const DisplayList& displayList, - const Vector3& lightCenter) - : mCanvasState(*this) { - ATRACE_NAME("prepare drawing commands"); - // Prepare to defer Fbo0 - mLayerReorderers.emplace_back(viewportWidth, viewportHeight, - Rect(viewportWidth, viewportHeight)); - mLayerStack.push_back(0); - mCanvasState.initializeSaveStack(viewportWidth, viewportHeight, - 0, 0, viewportWidth, viewportHeight, lightCenter); - - deferDisplayList(displayList); -} - void OpReorderer::onViewportInitialized() {} void OpReorderer::onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) {} @@ -462,10 +450,10 @@ void OpReorderer::deferNodePropsAndOps(RenderNode& node) { Matrix4::identity(), saveLayerBounds, &saveLayerPaint)); - deferDisplayList(*(node.getDisplayList())); + deferNodeOps(node); onEndLayerOp(*new (mAllocator) EndLayerOp()); } else { - deferDisplayList(*(node.getDisplayList())); + deferNodeOps(node); } } } @@ -610,18 +598,53 @@ void OpReorderer::deferShadow(const RenderNodeOp& casterNodeOp) { } } +void OpReorderer::deferProjectedChildren(const RenderNode& renderNode) { + const SkPath* projectionReceiverOutline = renderNode.properties().getOutline().getPath(); + int count = mCanvasState.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag); + + // can't be null, since DL=null node rejection happens before deferNodePropsAndOps + const DisplayList& displayList = *(renderNode.getDisplayList()); + + const RecordedOp* op = (displayList.getOps()[displayList.projectionReceiveIndex]); + const RenderNodeOp* backgroundOp = static_cast<const RenderNodeOp*>(op); + const RenderProperties& backgroundProps = backgroundOp->renderNode->properties(); + + // Transform renderer to match background we're projecting onto + // (by offsetting canvas by translationX/Y of background rendernode, since only those are set) + mCanvasState.translate(backgroundProps.getTranslationX(), backgroundProps.getTranslationY()); + + // If the projection receiver has an outline, we mask projected content to it + // (which we know, apriori, are all tessellated paths) + mCanvasState.setProjectionPathMask(mAllocator, projectionReceiverOutline); + + // draw projected nodes + for (size_t i = 0; i < renderNode.mProjectedNodes.size(); i++) { + RenderNodeOp* childOp = renderNode.mProjectedNodes[i]; + + int restoreTo = mCanvasState.save(SkCanvas::kMatrix_SaveFlag); + mCanvasState.concatMatrix(childOp->transformFromCompositingAncestor); + deferRenderNodeOp(*childOp); + mCanvasState.restoreToCount(restoreTo); + } + + mCanvasState.restoreToCount(count); +} + /** * Used to define a list of lambdas referencing private OpReorderer::onXXXXOp() methods. * - * This allows opIds embedded in the RecordedOps to be used for dispatching to these lambdas. E.g. a - * BitmapOp op then would be dispatched to OpReorderer::onBitmapOp(const BitmapOp&) + * This allows opIds embedded in the RecordedOps to be used for dispatching to these lambdas. + * E.g. a BitmapOp op then would be dispatched to OpReorderer::onBitmapOp(const BitmapOp&) */ #define OP_RECEIVER(Type) \ [](OpReorderer& reorderer, const RecordedOp& op) { reorderer.on##Type(static_cast<const Type&>(op)); }, -void OpReorderer::deferDisplayList(const DisplayList& displayList) { +void OpReorderer::deferNodeOps(const RenderNode& renderNode) { static std::function<void(OpReorderer& reorderer, const RecordedOp&)> receivers[] = { MAP_OPS(OP_RECEIVER) }; + + // can't be null, since DL=null node rejection happens before deferNodePropsAndOps + const DisplayList& displayList = *(renderNode.getDisplayList()); for (const DisplayList::Chunk& chunk : displayList.getChunks()) { FatVector<ZRenderNodeOpPair, 16> zTranslatedNodes; buildZSortedChildList(&zTranslatedNodes, displayList, chunk); @@ -630,6 +653,12 @@ void OpReorderer::deferDisplayList(const DisplayList& displayList) { for (size_t opIndex = chunk.beginOpIndex; opIndex < chunk.endOpIndex; opIndex++) { const RecordedOp* op = displayList.getOps()[opIndex]; receivers[op->opId](*this, *op); + + if (CC_UNLIKELY(!renderNode.mProjectedNodes.empty() + && displayList.projectionReceiveIndex >= 0 + && static_cast<int>(opIndex) == displayList.projectionReceiveIndex)) { + deferProjectedChildren(renderNode); + } } defer3dChildren(ChildrenSelectMode::Positive, zTranslatedNodes); } diff --git a/libs/hwui/OpReorderer.h b/libs/hwui/OpReorderer.h index 976f41323efa..00df8b0951e6 100644 --- a/libs/hwui/OpReorderer.h +++ b/libs/hwui/OpReorderer.h @@ -124,9 +124,6 @@ public: uint32_t viewportWidth, uint32_t viewportHeight, const std::vector< sp<RenderNode> >& nodes, const Vector3& lightCenter); - OpReorderer(int viewportWidth, int viewportHeight, const DisplayList& displayList, - const Vector3& lightCenter); - virtual ~OpReorderer() {} /** @@ -202,15 +199,17 @@ private: return BakedOpState::tryConstruct(mAllocator, *mCanvasState.currentSnapshot(), recordedOp); } - // should always be surrounded by a save/restore pair + // should always be surrounded by a save/restore pair, and not called if DisplayList is null void deferNodePropsAndOps(RenderNode& node); + template <typename V> + void defer3dChildren(ChildrenSelectMode mode, const V& zTranslatedNodes); + void deferShadow(const RenderNodeOp& casterOp); - void deferDisplayList(const DisplayList& displayList); + void deferProjectedChildren(const RenderNode& renderNode); - template <typename V> - void defer3dChildren(ChildrenSelectMode mode, const V& zTranslatedNodes); + void deferNodeOps(const RenderNode& renderNode); void deferRenderNodeOp(const RenderNodeOp& op); diff --git a/libs/hwui/RecordedOp.h b/libs/hwui/RecordedOp.h index 127dca5be440..b4a201ed2374 100644 --- a/libs/hwui/RecordedOp.h +++ b/libs/hwui/RecordedOp.h @@ -97,7 +97,17 @@ struct RenderNodeOp : RecordedOp { RenderNodeOp(BASE_PARAMS_PAINTLESS, RenderNode* renderNode) : SUPER_PAINTLESS(RenderNodeOp) , renderNode(renderNode) {} - RenderNode * renderNode; // not const, since drawing modifies it (somehow...) + RenderNode * renderNode; // not const, since drawing modifies it + + /** + * Holds the transformation between the projection surface ViewGroup and this RenderNode + * drawing instance. Represents any translations / transformations done within the drawing of + * the compositing ancestor ViewGroup's draw, before the draw of the View represented by this + * DisplayList draw instance. + * + * Note: doesn't include transformation within the RenderNode, or its properties. + */ + Matrix4 transformFromCompositingAncestor; bool skipInOrderDraw = false; }; diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h index 736cc9ecaf2a..6d0e9e0e0f7a 100644 --- a/libs/hwui/RecordingCanvas.h +++ b/libs/hwui/RecordingCanvas.h @@ -205,10 +205,6 @@ private: return dstBuffer; } - inline char* refText(const char* text, size_t byteLength) { - return (char*) refBuffer<uint8_t>((uint8_t*)text, byteLength); - } - inline const SkPath* refPath(const SkPath* path) { if (!path) return nullptr; @@ -220,13 +216,8 @@ private: } /** - * Returns a RenderThread-safe, const copy of the SkPaint parameter passed in (with deduping - * based on paint generation ID) - * - * Note that this forces Left_Align, since drawText glyph rendering expects left alignment, - * since alignment offsetting has been done at a higher level. This is done to essentially all - * copied paints, since the deduping can mean a paint is shared by drawText commands and other - * types (which wouldn't care about alignment). + * Returns a RenderThread-safe, const copy of the SkPaint parameter passed in + * (with deduping based on paint hash / equality check) */ inline const SkPaint* refPaint(const SkPaint* paint) { if (!paint) return nullptr; @@ -246,11 +237,8 @@ private: // In the unlikely event that 2 unique paints have the same hash we do a // object equality check to ensure we don't erroneously dedup them. if (cachedPaint == nullptr || *cachedPaint != *paint) { - SkPaint* copy = new SkPaint(*paint); - copy->setTextAlign(SkPaint::kLeft_Align); - - cachedPaint = copy; - mDisplayList->paints.emplace_back(copy); + cachedPaint = new SkPaint(*paint); + mDisplayList->paints.emplace_back(cachedPaint); // replaceValueFor() performs an add if the entry doesn't exist mPaintMap.replaceValueFor(key, cachedPaint); refBitmapsInShader(cachedPaint->getShader()); diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index 3f24f44e52da..ae690fdef4c7 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -487,7 +487,7 @@ void RenderNode::prepareSubTree(TreeInfo& info, bool functorsNeedLayer, DisplayL info.damageAccumulator->pushTransform(&op->localMatrix); bool childFunctorsNeedLayer = functorsNeedLayer; // TODO! || op->mRecordedWithPotentialStencilClip; #else - info.damageAccumulator->pushTransform(&op->mTransformFromParent); + info.damageAccumulator->pushTransform(&op->localMatrix); bool childFunctorsNeedLayer = functorsNeedLayer // Recorded with non-rect clip, or canvas-rotated by parent || op->mRecordedWithPotentialStencilClip; @@ -658,7 +658,6 @@ void RenderNode::applyViewPropertyTransforms(mat4& matrix, bool true3dTransform) * which are flagged to not draw in the standard draw loop. */ void RenderNode::computeOrdering() { -#if !HWUI_NEW_OPS ATRACE_CALL(); mProjectedNodes.clear(); @@ -666,43 +665,41 @@ void RenderNode::computeOrdering() { // transform properties are applied correctly to top level children if (mDisplayList == nullptr) return; for (unsigned int i = 0; i < mDisplayList->getChildren().size(); i++) { - DrawRenderNodeOp* childOp = mDisplayList->getChildren()[i]; + renderNodeOp_t* childOp = mDisplayList->getChildren()[i]; childOp->renderNode->computeOrderingImpl(childOp, &mProjectedNodes, &mat4::identity()); } -#endif } void RenderNode::computeOrderingImpl( - DrawRenderNodeOp* opState, - std::vector<DrawRenderNodeOp*>* compositedChildrenOfProjectionSurface, + renderNodeOp_t* opState, + std::vector<renderNodeOp_t*>* compositedChildrenOfProjectionSurface, const mat4* transformFromProjectionSurface) { -#if !HWUI_NEW_OPS mProjectedNodes.clear(); if (mDisplayList == nullptr || mDisplayList->isEmpty()) return; // TODO: should avoid this calculation in most cases // TODO: just calculate single matrix, down to all leaf composited elements Matrix4 localTransformFromProjectionSurface(*transformFromProjectionSurface); - localTransformFromProjectionSurface.multiply(opState->mTransformFromParent); + localTransformFromProjectionSurface.multiply(opState->localMatrix); if (properties().getProjectBackwards()) { // composited projectee, flag for out of order draw, save matrix, and store in proj surface - opState->mSkipInOrderDraw = true; - opState->mTransformFromCompositingAncestor = localTransformFromProjectionSurface; + opState->skipInOrderDraw = true; + opState->transformFromCompositingAncestor = localTransformFromProjectionSurface; compositedChildrenOfProjectionSurface->push_back(opState); } else { // standard in order draw - opState->mSkipInOrderDraw = false; + opState->skipInOrderDraw = false; } if (mDisplayList->getChildren().size() > 0) { const bool isProjectionReceiver = mDisplayList->projectionReceiveIndex >= 0; bool haveAppliedPropertiesToProjection = false; for (unsigned int i = 0; i < mDisplayList->getChildren().size(); i++) { - DrawRenderNodeOp* childOp = mDisplayList->getChildren()[i]; + renderNodeOp_t* childOp = mDisplayList->getChildren()[i]; RenderNode* child = childOp->renderNode; - std::vector<DrawRenderNodeOp*>* projectionChildren = nullptr; + std::vector<renderNodeOp_t*>* projectionChildren = nullptr; const mat4* projectionTransform = nullptr; if (isProjectionReceiver && !child->properties().getProjectBackwards()) { // if receiving projections, collect projecting descendant @@ -723,7 +720,6 @@ void RenderNode::computeOrderingImpl( child->computeOrderingImpl(childOp, projectionChildren, projectionTransform); } } -#endif } class DeferOperationHandler { @@ -793,10 +789,10 @@ void RenderNode::buildZSortedChildList(const DisplayList::Chunk& chunk, if (!MathUtils::isZero(childZ) && chunk.reorderChildren) { zTranslatedNodes.push_back(ZDrawRenderNodeOpPair(childZ, childOp)); - childOp->mSkipInOrderDraw = true; + childOp->skipInOrderDraw = true; } else if (!child->properties().getProjectBackwards()) { // regular, in order drawing DisplayList - childOp->mSkipInOrderDraw = false; + childOp->skipInOrderDraw = false; } } @@ -913,7 +909,7 @@ void RenderNode::issueOperationsOf3dChildren(ChildrenSelectMode mode, // attempt to render the shadow if the caster about to be drawn is its caster, // OR if its caster's Z value is similar to the previous potential caster if (shadowIndex == drawIndex || casterZ - lastCasterZ < SHADOW_DELTA) { - caster->issueDrawShadowOperation(casterOp->mTransformFromParent, handler); + caster->issueDrawShadowOperation(casterOp->localMatrix, handler); lastCasterZ = casterZ; // must do this even if current caster not casting a shadow shadowIndex++; @@ -927,10 +923,10 @@ void RenderNode::issueOperationsOf3dChildren(ChildrenSelectMode mode, DrawRenderNodeOp* childOp = zTranslatedNodes[drawIndex].value; - renderer.concatMatrix(childOp->mTransformFromParent); - childOp->mSkipInOrderDraw = false; // this is horrible, I'm so sorry everyone + renderer.concatMatrix(childOp->localMatrix); + childOp->skipInOrderDraw = false; // this is horrible, I'm so sorry everyone handler(childOp, renderer.getSaveCount() - 1, properties().getClipToBounds()); - childOp->mSkipInOrderDraw = true; + childOp->skipInOrderDraw = true; renderer.restoreToCount(restoreTo); drawIndex++; @@ -967,14 +963,14 @@ void RenderNode::issueOperationsOfProjectedChildren(OpenGLRenderer& renderer, T& // draw projected nodes for (size_t i = 0; i < mProjectedNodes.size(); i++) { - DrawRenderNodeOp* childOp = mProjectedNodes[i]; + renderNodeOp_t* childOp = mProjectedNodes[i]; // matrix save, concat, and restore can be done safely without allocating operations int restoreTo = renderer.save(SkCanvas::kMatrix_SaveFlag); - renderer.concatMatrix(childOp->mTransformFromCompositingAncestor); - childOp->mSkipInOrderDraw = false; // this is horrible, I'm so sorry everyone + renderer.concatMatrix(childOp->transformFromCompositingAncestor); + childOp->skipInOrderDraw = false; // this is horrible, I'm so sorry everyone handler(childOp, renderer.getSaveCount() - 1, properties().getClipToBounds()); - childOp->mSkipInOrderDraw = true; + childOp->skipInOrderDraw = true; renderer.restoreToCount(restoreTo); } diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h index 83d1b5888b64..b6f50b111ab5 100644 --- a/libs/hwui/RenderNode.h +++ b/libs/hwui/RenderNode.h @@ -51,20 +51,22 @@ class OpReorderer; class Rect; class SkiaShader; - #if HWUI_NEW_OPS class OffscreenBuffer; +struct RenderNodeOp; typedef OffscreenBuffer layer_t; +typedef RenderNodeOp renderNodeOp_t; #else class Layer; typedef Layer layer_t; +typedef DrawRenderNodeOp renderNodeOp_t; #endif class ClipRectOp; +class DrawRenderNodeOp; class SaveLayerOp; class SaveOp; class RestoreToCountOp; -class DrawRenderNodeOp; class TreeInfo; namespace proto { @@ -85,6 +87,7 @@ class RenderNode; */ class RenderNode : public VirtualLightRefBase { friend class TestUtils; // allow TestUtils to access syncDisplayList / syncProperties +friend class OpReorderer; public: enum DirtyPropertyMask { GENERIC = 1 << 1, @@ -221,8 +224,8 @@ private: PositiveZChildren }; - void computeOrderingImpl(DrawRenderNodeOp* opState, - std::vector<DrawRenderNodeOp*>* compositedChildrenOfProjectionSurface, + void computeOrderingImpl(renderNodeOp_t* opState, + std::vector<renderNodeOp_t*>* compositedChildrenOfProjectionSurface, const mat4* transformFromProjectionSurface); template <class T> @@ -305,7 +308,7 @@ private: */ // for projection surfaces, contains a list of all children items - std::vector<DrawRenderNodeOp*> mProjectedNodes; + std::vector<renderNodeOp_t*> mProjectedNodes; // How many references our parent(s) have to us. Typically this should alternate // between 2 and 1 (when a staging push happens we inc first then dec) diff --git a/libs/hwui/microbench/OpReordererBench.cpp b/libs/hwui/microbench/OpReordererBench.cpp index eea0c7f9ccbe..53b64c32ce52 100644 --- a/libs/hwui/microbench/OpReordererBench.cpp +++ b/libs/hwui/microbench/OpReordererBench.cpp @@ -17,11 +17,14 @@ #include <benchmark/Benchmark.h> #include "BakedOpState.h" +#include "BakedOpDispatcher.h" #include "BakedOpRenderer.h" +#include "LayerUpdateQueue.h" #include "OpReorderer.h" #include "RecordedOp.h" #include "RecordingCanvas.h" #include "utils/TestUtils.h" +#include "Vector.h" #include "microbench/MicroBench.h" #include <vector> @@ -29,26 +32,38 @@ using namespace android; using namespace android::uirenderer; -auto sReorderingDisplayList = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { - SkBitmap bitmap = TestUtils::createSkBitmap(10, 10); - SkPaint paint; +const LayerUpdateQueue sEmptyLayerUpdateQueue; +const Vector3 sLightCenter = {100, 100, 100}; - // Alternate between drawing rects and bitmaps, with bitmaps overlapping rects. - // Rects don't overlap bitmaps, so bitmaps should be brought to front as a group. - canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag); - for (int i = 0; i < 30; i++) { - canvas.translate(0, 10); - canvas.drawRect(0, 0, 10, 10, paint); - canvas.drawBitmap(bitmap, 5, 0, nullptr); - } - canvas.restore(); -}); +static std::vector<sp<RenderNode>> createTestNodeList() { + auto node = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& props, RecordingCanvas& canvas) { + SkBitmap bitmap = TestUtils::createSkBitmap(10, 10); + SkPaint paint; + + // Alternate between drawing rects and bitmaps, with bitmaps overlapping rects. + // Rects don't overlap bitmaps, so bitmaps should be brought to front as a group. + canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag); + for (int i = 0; i < 30; i++) { + canvas.translate(0, 10); + canvas.drawRect(0, 0, 10, 10, paint); + canvas.drawBitmap(bitmap, 5, 0, nullptr); + } + canvas.restore(); + }); + TestUtils::syncHierarchyPropertiesAndDisplayList(node); + std::vector<sp<RenderNode>> vec; + vec.emplace_back(node); + return vec; +} BENCHMARK_NO_ARG(BM_OpReorderer_defer); void BM_OpReorderer_defer::Run(int iters) { + auto nodes = createTestNodeList(); StartBenchmarkTiming(); for (int i = 0; i < iters; i++) { - OpReorderer reorderer(200, 200, *sReorderingDisplayList, (Vector3) { 100, 100, 100 }); + OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 200), 100, 200, + nodes, sLightCenter); MicroBench::DoNotOptimize(&reorderer); } StopBenchmarkTiming(); @@ -57,13 +72,16 @@ void BM_OpReorderer_defer::Run(int iters) { BENCHMARK_NO_ARG(BM_OpReorderer_deferAndRender); void BM_OpReorderer_deferAndRender::Run(int iters) { TestUtils::runOnRenderThread([this, iters](renderthread::RenderThread& thread) { + auto nodes = createTestNodeList(); + BakedOpRenderer::LightInfo lightInfo = {50.0f, 128, 128 }; + RenderState& renderState = thread.renderState(); Caches& caches = Caches::getInstance(); - BakedOpRenderer::LightInfo lightInfo = { 50.0f, 128, 128 }; StartBenchmarkTiming(); for (int i = 0; i < iters; i++) { - OpReorderer reorderer(200, 200, *sReorderingDisplayList, (Vector3) { 100, 100, 100 }); + OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 200), 100, 200, + nodes, sLightCenter); BakedOpRenderer renderer(caches, renderState, true, lightInfo); reorderer.replayBakedOps<BakedOpDispatcher>(renderer); diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index c3cfc940281e..d36ce993164e 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -28,6 +28,7 @@ #include "renderthread/RenderThread.h" #if HWUI_NEW_OPS +#include "BakedOpDispatcher.h" #include "BakedOpRenderer.h" #endif diff --git a/libs/hwui/tests/scenes/OvalAnimation.cpp b/libs/hwui/tests/scenes/OvalAnimation.cpp index 919a53d118b3..936aba184c88 100644 --- a/libs/hwui/tests/scenes/OvalAnimation.cpp +++ b/libs/hwui/tests/scenes/OvalAnimation.cpp @@ -29,17 +29,14 @@ public: sp<RenderNode> card; void createContent(int width, int height, TestCanvas& canvas) override { canvas.drawColor(0xFFFFFFFF, SkXfermode::kSrcOver_Mode); - canvas.insertReorderBarrier(true); - - card = TestUtils::createNode(0, 0, 200, 200, [](TestCanvas& canvas) { + card = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& props, TestCanvas& canvas) { SkPaint paint; paint.setAntiAlias(true); paint.setColor(0xFF000000); canvas.drawOval(0, 0, 200, 200, paint); }); - canvas.drawRenderNode(card.get()); - canvas.insertReorderBarrier(false); } void doFrame(int frameNr) override { diff --git a/libs/hwui/tests/scenes/PartialDamageAnimation.cpp b/libs/hwui/tests/scenes/PartialDamageAnimation.cpp index 0fba4eb5f9ff..c31ddd1d531b 100644 --- a/libs/hwui/tests/scenes/PartialDamageAnimation.cpp +++ b/libs/hwui/tests/scenes/PartialDamageAnimation.cpp @@ -44,7 +44,7 @@ public: SkColor color = COLORS[static_cast<int>((y / dp(116))) % 4]; sp<RenderNode> card = TestUtils::createNode(x, y, x + dp(100), y + dp(100), - [color](TestCanvas& canvas) { + [color](RenderProperties& props, TestCanvas& canvas) { canvas.drawColor(color, SkXfermode::kSrcOver_Mode); }); canvas.drawRenderNode(card.get()); diff --git a/libs/hwui/tests/scenes/RectGridAnimation.cpp b/libs/hwui/tests/scenes/RectGridAnimation.cpp index 254f8280cb66..a1f04d67c785 100644 --- a/libs/hwui/tests/scenes/RectGridAnimation.cpp +++ b/libs/hwui/tests/scenes/RectGridAnimation.cpp @@ -34,7 +34,7 @@ public: canvas.insertReorderBarrier(true); card = TestUtils::createNode(50, 50, 250, 250, - [](TestCanvas& canvas) { + [](RenderProperties& props, TestCanvas& canvas) { canvas.drawColor(0xFFFF00FF, SkXfermode::kSrcOver_Mode); SkRegion region; diff --git a/libs/hwui/tests/scenes/SaveLayerAnimation.cpp b/libs/hwui/tests/scenes/SaveLayerAnimation.cpp index c62dd19968c3..c73e97ba003c 100644 --- a/libs/hwui/tests/scenes/SaveLayerAnimation.cpp +++ b/libs/hwui/tests/scenes/SaveLayerAnimation.cpp @@ -32,7 +32,7 @@ public: canvas.drawColor(0xFFFFFFFF, SkXfermode::kSrcOver_Mode); // background card = TestUtils::createNode(0, 0, 200, 200, - [](TestCanvas& canvas) { + [](RenderProperties& props, TestCanvas& canvas) { canvas.saveLayerAlpha(0, 0, 200, 200, 128, SkCanvas::kClipToLayer_SaveFlag); canvas.drawColor(0xFF00FF00, SkXfermode::kSrcOver_Mode); // outer, unclipped canvas.saveLayerAlpha(50, 50, 150, 150, 128, SkCanvas::kClipToLayer_SaveFlag); diff --git a/libs/hwui/unit_tests/LayerUpdateQueueTests.cpp b/libs/hwui/unit_tests/LayerUpdateQueueTests.cpp index 05fd08a3340a..cc15cc6d2628 100644 --- a/libs/hwui/unit_tests/LayerUpdateQueueTests.cpp +++ b/libs/hwui/unit_tests/LayerUpdateQueueTests.cpp @@ -31,7 +31,7 @@ TEST(LayerUpdateQueue, construct) { // sync node properties, so properties() reflects correct width and height static sp<RenderNode> createSyncedNode(uint32_t width, uint32_t height) { - sp<RenderNode> node = TestUtils::createNode(0, 0, width, height); + sp<RenderNode> node = TestUtils::createNode(0, 0, width, height, nullptr); TestUtils::syncHierarchyPropertiesAndDisplayList(node); return node; } diff --git a/libs/hwui/unit_tests/OpReordererTests.cpp b/libs/hwui/unit_tests/OpReordererTests.cpp index d76086c9cfcd..2ce1d0a5db7f 100644 --- a/libs/hwui/unit_tests/OpReordererTests.cpp +++ b/libs/hwui/unit_tests/OpReordererTests.cpp @@ -111,25 +111,29 @@ TEST(OpReorderer, simple) { } }; - auto dl = TestUtils::createDisplayList<RecordingCanvas>(100, 200, [](RecordingCanvas& canvas) { + auto node = TestUtils::createNode(0, 0, 100, 200, + [](RenderProperties& props, RecordingCanvas& canvas) { SkBitmap bitmap = TestUtils::createSkBitmap(25, 25); canvas.drawRect(0, 0, 100, 200, SkPaint()); canvas.drawBitmap(bitmap, 10, 10, nullptr); }); - OpReorderer reorderer(100, 200, *dl, sLightCenter); + OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 200), 100, 200, + createSyncedNodeList(node), sLightCenter); SimpleTestRenderer renderer; reorderer.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(4, renderer.getIndex()); // 2 ops + start + end } TEST(OpReorderer, simpleRejection) { - auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { + auto node = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& props, RecordingCanvas& canvas) { canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag); canvas.clipRect(200, 200, 400, 400, SkRegion::kIntersect_Op); // intersection should be empty canvas.drawRect(0, 0, 400, 400, SkPaint()); canvas.restore(); }); - OpReorderer reorderer(200, 200, *dl, sLightCenter); + OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200, + createSyncedNodeList(node), sLightCenter); FailRenderer renderer; reorderer.replayBakedOps<TestDispatcher>(renderer); @@ -147,7 +151,8 @@ TEST(OpReorderer, simpleBatching) { } }; - auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { + auto node = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& props, RecordingCanvas& canvas) { SkBitmap bitmap = TestUtils::createSkBitmap(10, 10); // Alternate between drawing rects and bitmaps, with bitmaps overlapping rects. @@ -161,7 +166,8 @@ TEST(OpReorderer, simpleBatching) { canvas.restore(); }); - OpReorderer reorderer(200, 200, *dl, sLightCenter); + OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200, + createSyncedNodeList(node), sLightCenter); SimpleBatchingTestRenderer renderer; reorderer.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2 * LOOPS, renderer.getIndex()) @@ -179,16 +185,19 @@ TEST(OpReorderer, textStrikethroughBatching) { EXPECT_TRUE(mIndex++ < LOOPS) << "Text should be beneath all strikethrough rects"; } }; - auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 2000, [](RecordingCanvas& canvas) { + auto node = TestUtils::createNode(0, 0, 200, 2000, + [](RenderProperties& props, RecordingCanvas& canvas) { SkPaint textPaint; textPaint.setAntiAlias(true); textPaint.setTextSize(20); textPaint.setStrikeThruText(true); + textPaint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); for (int i = 0; i < LOOPS; i++) { TestUtils::drawTextToCanvas(&canvas, "test text", textPaint, 10, 100 * (i + 1)); } }); - OpReorderer reorderer(200, 2000, *dl, sLightCenter); + OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 2000), 200, 2000, + createSyncedNodeList(node), sLightCenter); TextStrikethroughTestRenderer renderer; reorderer.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2 * LOOPS, renderer.getIndex()) @@ -214,14 +223,16 @@ TEST(OpReorderer, renderNode) { } }; - sp<RenderNode> child = TestUtils::createNode(10, 10, 110, 110, [](RecordingCanvas& canvas) { + auto child = TestUtils::createNode(10, 10, 110, 110, + [](RenderProperties& props, RecordingCanvas& canvas) { SkPaint paint; paint.setColor(SK_ColorWHITE); canvas.drawRect(0, 0, 100, 100, paint); }); RenderNode* childPtr = child.get(); - sp<RenderNode> parent = TestUtils::createNode(0, 0, 200, 200, [childPtr](RecordingCanvas& canvas) { + auto parent = TestUtils::createNode(0, 0, 200, 200, + [childPtr](RenderProperties& props, RecordingCanvas& canvas) { SkPaint paint; paint.setColor(SK_ColorDKGRAY); canvas.drawRect(0, 0, 200, 200, paint); @@ -249,7 +260,8 @@ TEST(OpReorderer, clipped) { } }; - sp<RenderNode> node = TestUtils::createNode(0, 0, 200, 200, [](RecordingCanvas& canvas) { + auto node = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& props, RecordingCanvas& canvas) { SkBitmap bitmap = TestUtils::createSkBitmap(200, 200); canvas.drawBitmap(bitmap, 0, 0, nullptr); }); @@ -291,13 +303,14 @@ TEST(OpReorderer, saveLayerSimple) { } }; - auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { + auto node = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& props, RecordingCanvas& canvas) { canvas.saveLayerAlpha(10, 10, 190, 190, 128, SkCanvas::kClipToLayer_SaveFlag); canvas.drawRect(10, 10, 190, 190, SkPaint()); canvas.restore(); }); - - OpReorderer reorderer(200, 200, *dl, sLightCenter); + OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200, + createSyncedNodeList(node), sLightCenter); SaveLayerSimpleTestRenderer renderer; reorderer.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(4, renderer.getIndex()); @@ -354,7 +367,8 @@ TEST(OpReorderer, saveLayerNested) { } }; - auto dl = TestUtils::createDisplayList<RecordingCanvas>(800, 800, [](RecordingCanvas& canvas) { + auto node = TestUtils::createNode(0, 0, 800, 800, + [](RenderProperties& props, RecordingCanvas& canvas) { canvas.saveLayerAlpha(0, 0, 800, 800, 128, SkCanvas::kClipToLayer_SaveFlag); { canvas.drawRect(0, 0, 800, 800, SkPaint()); @@ -367,14 +381,16 @@ TEST(OpReorderer, saveLayerNested) { canvas.restore(); }); - OpReorderer reorderer(800, 800, *dl, sLightCenter); + OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(800, 800), 800, 800, + createSyncedNodeList(node), sLightCenter); SaveLayerNestedTestRenderer renderer; reorderer.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(10, renderer.getIndex()); } TEST(OpReorderer, saveLayerContentRejection) { - auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { + auto node = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& props, RecordingCanvas& canvas) { canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag); canvas.clipRect(200, 200, 400, 400, SkRegion::kIntersect_Op); canvas.saveLayerAlpha(200, 200, 400, 400, 128, SkCanvas::kClipToLayer_SaveFlag); @@ -385,7 +401,8 @@ TEST(OpReorderer, saveLayerContentRejection) { canvas.restore(); canvas.restore(); }); - OpReorderer reorderer(200, 200, *dl, sLightCenter); + OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(200, 200), 200, 200, + createSyncedNodeList(node), sLightCenter); FailRenderer renderer; // should see no ops, even within the layer, since the layer should be rejected @@ -424,7 +441,7 @@ RENDERTHREAD_TEST(OpReorderer, hwLayerSimple) { } }; - sp<RenderNode> node = TestUtils::createNode(10, 10, 110, 110, + auto node = TestUtils::createNode(10, 10, 110, 110, [](RenderProperties& props, RecordingCanvas& canvas) { props.mutateLayerProperties().setType(LayerType::RenderLayer); SkPaint paint; @@ -562,7 +579,7 @@ static void drawOrderedRect(RecordingCanvas* canvas, uint8_t expectedDrawOrder) } static void drawOrderedNode(RecordingCanvas* canvas, uint8_t expectedDrawOrder, float z) { auto node = TestUtils::createNode(0, 0, 100, 100, - [expectedDrawOrder](RecordingCanvas& canvas) { + [expectedDrawOrder](RenderProperties& props, RecordingCanvas& canvas) { drawOrderedRect(&canvas, expectedDrawOrder); }); node->mutateStagingProperties().setTranslationZ(z); @@ -579,7 +596,7 @@ TEST(OpReorderer, zReorder) { }; auto parent = TestUtils::createNode(0, 0, 100, 100, - [](RecordingCanvas& canvas) { + [](RenderProperties& props, RecordingCanvas& canvas) { drawOrderedNode(&canvas, 0, 10.0f); // in reorder=false at this point, so played inorder drawOrderedRect(&canvas, 1); canvas.insertReorderBarrier(true); @@ -600,10 +617,93 @@ TEST(OpReorderer, zReorder) { EXPECT_EQ(10, renderer.getIndex()); }; +TEST(OpReorderer, projectionReorder) { + static const int scrollX = 5; + static const int scrollY = 10; + class ProjectionReorderTestRenderer : public TestRendererBase { + public: + void onRectOp(const RectOp& op, const BakedOpState& state) override { + const int index = mIndex++; + + Matrix4 expectedMatrix; + switch (index) { + case 0: + EXPECT_EQ(Rect(100, 100), op.unmappedBounds); + EXPECT_EQ(SK_ColorWHITE, op.paint->getColor()); + expectedMatrix.loadIdentity(); + break; + case 1: + EXPECT_EQ(Rect(-10, -10, 60, 60), op.unmappedBounds); + EXPECT_EQ(SK_ColorDKGRAY, op.paint->getColor()); + expectedMatrix.loadTranslate(50, 50, 0); // TODO: should scroll be respected here? + break; + case 2: + EXPECT_EQ(Rect(100, 50), op.unmappedBounds); + EXPECT_EQ(SK_ColorBLUE, op.paint->getColor()); + expectedMatrix.loadTranslate(-scrollX, 50 - scrollY, 0); + break; + default: + ADD_FAILURE(); + } + EXPECT_MATRIX_APPROX_EQ(expectedMatrix, state.computedState.transform); + } + }; + + /** + * Construct a tree of nodes, where the root (A) has a receiver background (B), and a child (C) + * with a projecting child (P) of its own. P would normally draw between B and C's "background" + * draw, but because it is projected backwards, it's drawn in between B and C. + * + * The parent is scrolled by scrollX/scrollY, but this does not affect the background + * (which isn't affected by scroll). + */ + auto receiverBackground = TestUtils::createNode(0, 0, 100, 100, + [](RenderProperties& properties, RecordingCanvas& canvas) { + properties.setProjectionReceiver(true); + // scroll doesn't apply to background, so undone via translationX/Y + // NOTE: translationX/Y only! no other transform properties may be set for a proj receiver! + properties.setTranslationX(scrollX); + properties.setTranslationY(scrollY); + + SkPaint paint; + paint.setColor(SK_ColorWHITE); + canvas.drawRect(0, 0, 100, 100, paint); + }); + auto projectingRipple = TestUtils::createNode(50, 0, 100, 50, + [](RenderProperties& properties, RecordingCanvas& canvas) { + properties.setProjectBackwards(true); + properties.setClipToBounds(false); + SkPaint paint; + paint.setColor(SK_ColorDKGRAY); + canvas.drawRect(-10, -10, 60, 60, paint); + }); + auto child = TestUtils::createNode(0, 50, 100, 100, + [&projectingRipple](RenderProperties& properties, RecordingCanvas& canvas) { + SkPaint paint; + paint.setColor(SK_ColorBLUE); + canvas.drawRect(0, 0, 100, 50, paint); + canvas.drawRenderNode(projectingRipple.get()); + }); + auto parent = TestUtils::createNode(0, 0, 100, 100, + [&receiverBackground, &child](RenderProperties& properties, RecordingCanvas& canvas) { + canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag); + canvas.translate(-scrollX, -scrollY); // Apply scroll (note: bg undoes this internally) + canvas.drawRenderNode(receiverBackground.get()); + canvas.drawRenderNode(child.get()); + canvas.restore(); + }); + + OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 100), 100, 100, + createSyncedNodeList(parent), sLightCenter); + ProjectionReorderTestRenderer renderer; + reorderer.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(3, renderer.getIndex()); +} + // creates a 100x100 shadow casting node with provided translationZ static sp<RenderNode> createWhiteRectShadowCaster(float translationZ) { return TestUtils::createNode(0, 0, 100, 100, - [translationZ] (RenderProperties& properties, RecordingCanvas& canvas) { + [translationZ](RenderProperties& properties, RecordingCanvas& canvas) { properties.setTranslationZ(translationZ); properties.mutableOutline().setRoundRect(0, 0, 100, 100, 0.0f, 1.0f); SkPaint paint; @@ -630,8 +730,8 @@ TEST(OpReorderer, shadow) { } }; - sp<RenderNode> parent = TestUtils::createNode(0, 0, 200, 200, - [] (RecordingCanvas& canvas) { + auto parent = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& props, RecordingCanvas& canvas) { canvas.insertReorderBarrier(true); canvas.drawRenderNode(createWhiteRectShadowCaster(5.0f).get()); }); @@ -666,8 +766,8 @@ TEST(OpReorderer, shadowSaveLayer) { } }; - sp<RenderNode> parent = TestUtils::createNode(0, 0, 200, 200, - [] (RecordingCanvas& canvas) { + auto parent = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& props, RecordingCanvas& canvas) { // save/restore outside of reorderBarrier, so they don't get moved out of place canvas.translate(20, 10); int count = canvas.saveLayerAlpha(30, 50, 130, 150, 128, SkCanvas::kClipToLayer_SaveFlag); @@ -706,7 +806,7 @@ RENDERTHREAD_TEST(OpReorderer, shadowHwLayer) { } }; - sp<RenderNode> parent = TestUtils::createNode(50, 60, 150, 160, + auto parent = TestUtils::createNode(50, 60, 150, 160, [](RenderProperties& props, RecordingCanvas& canvas) { props.mutateLayerProperties().setType(LayerType::RenderLayer); canvas.insertReorderBarrier(true); @@ -749,8 +849,8 @@ TEST(OpReorderer, shadowLayering) { EXPECT_TRUE(index == 2 || index == 3); } }; - sp<RenderNode> parent = TestUtils::createNode(0, 0, 200, 200, - [] (RecordingCanvas& canvas) { + auto parent = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& props, RecordingCanvas& canvas) { canvas.insertReorderBarrier(true); canvas.drawRenderNode(createWhiteRectShadowCaster(5.0f).get()); canvas.drawRenderNode(createWhiteRectShadowCaster(5.0001f).get()); @@ -954,7 +1054,7 @@ TEST(OpReorderer, renderPropSaveLayerAlphaRotate) { SaveLayerAlphaData observedData; testSaveLayerAlphaClip(&observedData, [](RenderProperties& properties) { // Translate and rotate the view so that the only visible part is the top left corner of - // the view. It will form an isoceles right triangle with a long side length of 200 at the + // the view. It will form an isosceles right triangle with a long side length of 200 at the // bottom of the viewport. properties.setTranslationX(100); properties.setTranslationY(100); diff --git a/libs/hwui/unit_tests/RecordingCanvasTests.cpp b/libs/hwui/unit_tests/RecordingCanvasTests.cpp index c23d47e3fab4..81f0851a6174 100644 --- a/libs/hwui/unit_tests/RecordingCanvasTests.cpp +++ b/libs/hwui/unit_tests/RecordingCanvasTests.cpp @@ -75,6 +75,7 @@ TEST(RecordingCanvas, drawText) { SkPaint paint; paint.setAntiAlias(true); paint.setTextSize(20); + paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); TestUtils::drawTextToCanvas(&canvas, "test text", paint, 25, 25); }); @@ -95,6 +96,7 @@ TEST(RecordingCanvas, drawText_strikeThruAndUnderline) { SkPaint paint; paint.setAntiAlias(true); paint.setTextSize(20); + paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); for (int i = 0; i < 2; i++) { for (int j = 0; j < 2; j++) { paint.setUnderlineText(i != 0); @@ -126,6 +128,7 @@ TEST(RecordingCanvas, drawText_forceAlignLeft) { SkPaint paint; paint.setAntiAlias(true); paint.setTextSize(20); + paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); paint.setTextAlign(SkPaint::kLeft_Align); TestUtils::drawTextToCanvas(&canvas, "test text", paint, 25, 25); paint.setTextAlign(SkPaint::kCenter_Align); @@ -135,12 +138,17 @@ TEST(RecordingCanvas, drawText_forceAlignLeft) { }); int count = 0; - playbackOps(*dl, [&count](const RecordedOp& op) { + float lastX = FLT_MAX; + playbackOps(*dl, [&count, &lastX](const RecordedOp& op) { count++; ASSERT_EQ(RecordedOpId::TextOp, op.opId); EXPECT_EQ(SkPaint::kLeft_Align, op.paint->getTextAlign()) << "recorded drawText commands must force kLeft_Align on their paint"; - EXPECT_EQ(SkPaint::kGlyphID_TextEncoding, op.paint->getTextEncoding()); // verify TestUtils + + // verify TestUtils alignment offsetting (TODO: move asserts to Canvas base class) + EXPECT_GT(lastX, ((const TextOp&)op).x) + << "x coordinate should reduce across each of the draw commands, from alignment"; + lastX = ((const TextOp&)op).x; }); ASSERT_EQ(3, count); } @@ -313,6 +321,49 @@ TEST(RecordingCanvas, saveLayer_rotateClipped) { EXPECT_EQ(3, count); } +TEST(RecordingCanvas, drawRenderNode_projection) { + sp<RenderNode> background = TestUtils::createNode(50, 50, 150, 150, + [](RenderProperties& props, RecordingCanvas& canvas) { + SkPaint paint; + paint.setColor(SK_ColorWHITE); + canvas.drawRect(0, 0, 100, 100, paint); + }); + { + background->mutateStagingProperties().setProjectionReceiver(false); + + // NO RECEIVER PRESENT + auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, + [&background](RecordingCanvas& canvas) { + canvas.drawRect(0, 0, 100, 100, SkPaint()); + canvas.drawRenderNode(background.get()); + canvas.drawRect(0, 0, 100, 100, SkPaint()); + }); + EXPECT_EQ(-1, dl->projectionReceiveIndex) + << "no projection receiver should have been observed"; + } + { + background->mutateStagingProperties().setProjectionReceiver(true); + + // RECEIVER PRESENT + auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, + [&background](RecordingCanvas& canvas) { + canvas.drawRect(0, 0, 100, 100, SkPaint()); + canvas.drawRenderNode(background.get()); + canvas.drawRect(0, 0, 100, 100, SkPaint()); + }); + + ASSERT_EQ(3u, dl->getOps().size()) << "Must be three ops"; + auto op = dl->getOps()[1]; + EXPECT_EQ(RecordedOpId::RenderNodeOp, op->opId); + EXPECT_EQ(1, dl->projectionReceiveIndex) + << "correct projection receiver not identified"; + + // verify the behavior works even though projection receiver hasn't been sync'd yet + EXPECT_TRUE(background->stagingProperties().isProjectionReceiver()); + EXPECT_FALSE(background->properties().isProjectionReceiver()); + } +} + TEST(RecordingCanvas, insertReorderBarrier) { auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { canvas.drawRect(0, 0, 400, 400, SkPaint()); @@ -334,5 +385,39 @@ TEST(RecordingCanvas, insertReorderBarrier) { EXPECT_TRUE(chunks[1].reorderChildren); } +TEST(RecordingCanvas, refPaint) { + SkPaint paint; + paint.setAntiAlias(true); + paint.setTextSize(20); + paint.setTextAlign(SkPaint::kLeft_Align); + paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); + + auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [&paint](RecordingCanvas& canvas) { + paint.setColor(SK_ColorBLUE); + // first three should use same paint + canvas.drawRect(0, 0, 200, 10, paint); + SkPaint paintCopy(paint); + canvas.drawRect(0, 10, 200, 20, paintCopy); + TestUtils::drawTextToCanvas(&canvas, "helloworld", paint, 50, 25); + + // only here do we use different paint ptr + paint.setColor(SK_ColorRED); + canvas.drawRect(0, 20, 200, 30, paint); + }); + auto ops = dl->getOps(); + ASSERT_EQ(4u, ops.size()); + + // first three are the same + EXPECT_NE(nullptr, ops[0]->paint); + EXPECT_NE(&paint, ops[0]->paint); + EXPECT_EQ(ops[0]->paint, ops[1]->paint); + EXPECT_EQ(ops[0]->paint, ops[2]->paint); + + // last is different, but still copied / non-null + EXPECT_NE(nullptr, ops[3]->paint); + EXPECT_NE(ops[0]->paint, ops[3]->paint); + EXPECT_NE(&paint, ops[3]->paint); +} + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/utils/TestUtils.cpp b/libs/hwui/utils/TestUtils.cpp index dd6fc36b8042..6cef85203352 100644 --- a/libs/hwui/utils/TestUtils.cpp +++ b/libs/hwui/utils/TestUtils.cpp @@ -37,44 +37,56 @@ SkColor TestUtils::interpolateColor(float fraction, SkColor start, SkColor end) } void TestUtils::drawTextToCanvas(TestCanvas* canvas, const char* text, - const SkPaint& inPaint, float x, float y) { - // copy to force TextEncoding (which JNI layer would have done) - SkPaint paint(inPaint); - paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); + const SkPaint& paint, float x, float y) { + // drawing text requires GlyphID TextEncoding (which JNI layer would have done) + LOG_ALWAYS_FATAL_IF(paint.getTextEncoding() != SkPaint::kGlyphID_TextEncoding, + "must use glyph encoding"); - SkMatrix identity; - identity.setIdentity(); - SkSurfaceProps surfaceProps(0, kUnknown_SkPixelGeometry); - SkAutoGlyphCacheNoGamma autoCache(paint, &surfaceProps, &identity); + SkMatrix identity; + identity.setIdentity(); + SkSurfaceProps surfaceProps(0, kUnknown_SkPixelGeometry); + SkAutoGlyphCacheNoGamma autoCache(paint, &surfaceProps, &identity); - float totalAdvance = 0; - std::vector<glyph_t> glyphs; - std::vector<float> positions; - Rect bounds; - while (*text != '\0') { - SkUnichar unichar = SkUTF8_NextUnichar(&text); - glyph_t glyph = autoCache.getCache()->unicharToGlyph(unichar); - autoCache.getCache()->unicharToGlyph(unichar); + float totalAdvance = 0; + std::vector<glyph_t> glyphs; + std::vector<float> positions; + Rect bounds; + while (*text != '\0') { + SkUnichar unichar = SkUTF8_NextUnichar(&text); + glyph_t glyph = autoCache.getCache()->unicharToGlyph(unichar); + autoCache.getCache()->unicharToGlyph(unichar); - // push glyph and its relative position - glyphs.push_back(glyph); - positions.push_back(totalAdvance); - positions.push_back(0); + // push glyph and its relative position + glyphs.push_back(glyph); + positions.push_back(totalAdvance); + positions.push_back(0); - // compute bounds - SkGlyph skGlyph = autoCache.getCache()->getUnicharMetrics(unichar); - Rect glyphBounds(skGlyph.fWidth, skGlyph.fHeight); - glyphBounds.translate(totalAdvance + skGlyph.fLeft, skGlyph.fTop); - bounds.unionWith(glyphBounds); + // compute bounds + SkGlyph skGlyph = autoCache.getCache()->getUnicharMetrics(unichar); + Rect glyphBounds(skGlyph.fWidth, skGlyph.fHeight); + glyphBounds.translate(totalAdvance + skGlyph.fLeft, skGlyph.fTop); + bounds.unionWith(glyphBounds); - // advance next character - SkScalar skWidth; - paint.getTextWidths(&glyph, sizeof(glyph), &skWidth, NULL); - totalAdvance += skWidth; - } - bounds.translate(x, y); - canvas->drawText(glyphs.data(), positions.data(), glyphs.size(), paint, x, y, - bounds.left, bounds.top, bounds.right, bounds.bottom, totalAdvance); + // advance next character + SkScalar skWidth; + paint.getTextWidths(&glyph, sizeof(glyph), &skWidth, NULL); + totalAdvance += skWidth; + } + + // apply alignment via x parameter (which JNI layer would have done) + if (paint.getTextAlign() == SkPaint::kCenter_Align) { + x -= totalAdvance / 2; + } else if (paint.getTextAlign() == SkPaint::kRight_Align) { + x -= totalAdvance; + } + + bounds.translate(x, y); + + // Force left alignment, since alignment offset is already baked in + SkPaint alignPaintCopy(paint); + alignPaintCopy.setTextAlign(SkPaint::kLeft_Align); + canvas->drawText(glyphs.data(), positions.data(), glyphs.size(), alignPaintCopy, x, y, + bounds.left, bounds.top, bounds.right, bounds.bottom, totalAdvance); } } /* namespace uirenderer */ diff --git a/libs/hwui/utils/TestUtils.h b/libs/hwui/utils/TestUtils.h index f9fa242ec833..9c1c0b9d2c08 100644 --- a/libs/hwui/utils/TestUtils.h +++ b/libs/hwui/utils/TestUtils.h @@ -121,7 +121,7 @@ public: } static sp<RenderNode> createNode(int left, int top, int right, int bottom, - std::function<void(RenderProperties& props, TestCanvas& canvas)> setup = nullptr) { + std::function<void(RenderProperties& props, TestCanvas& canvas)> setup) { #if HWUI_NULL_GPU // if RenderNodes are being sync'd/used, device info will be needed, since // DeviceInfo::maxTextureSize() affects layer property @@ -140,22 +140,6 @@ public: return node; } - static sp<RenderNode> createNode(int left, int top, int right, int bottom, - std::function<void(RenderProperties& props)> setup) { - return createNode(left, top, right, bottom, - [&setup](RenderProperties& props, TestCanvas& canvas) { - setup(props); - }); - } - - static sp<RenderNode> createNode(int left, int top, int right, int bottom, - std::function<void(TestCanvas& canvas)> setup) { - return createNode(left, top, right, bottom, - [&setup](RenderProperties& props, TestCanvas& canvas) { - setup(canvas); - }); - } - static void recordNode(RenderNode& node, std::function<void(TestCanvas&)> contentCallback) { TestCanvas canvas(node.stagingProperties().getWidth(), @@ -164,6 +148,13 @@ public: node.setStagingDisplayList(canvas.finishRecording()); } + /** + * Forces a sync of a tree of RenderNode, such that every descendant will have its staging + * properties and DisplayList moved to the render copies. + * + * Note: does not check dirtiness bits, so any non-staging DisplayLists will be discarded. + * For this reason, this should generally only be called once on a tree. + */ static void syncHierarchyPropertiesAndDisplayList(sp<RenderNode>& node) { syncHierarchyPropertiesAndDisplayListImpl(node.get()); } @@ -197,7 +188,7 @@ public: static SkColor interpolateColor(float fraction, SkColor start, SkColor end); static void drawTextToCanvas(TestCanvas* canvas, const char* text, - const SkPaint& inPaint, float x, float y); + const SkPaint& paint, float x, float y); private: static void syncHierarchyPropertiesAndDisplayListImpl(RenderNode* node) { diff --git a/packages/Shell/Android.mk b/packages/Shell/Android.mk index 5bd48c63433c..f8c13d63327f 100644 --- a/packages/Shell/Android.mk +++ b/packages/Shell/Android.mk @@ -3,7 +3,7 @@ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional -LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 @@ -12,3 +12,5 @@ LOCAL_CERTIFICATE := platform LOCAL_PRIVILEGED_MODULE := true include $(BUILD_PACKAGE) + +include $(LOCAL_PATH)/tests/Android.mk diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 8c39ee654e6d..bf3982d9a71d 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <!-- /* - * Copyright (c) 2014 Google Inc. + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -147,5 +147,9 @@ <action android:name="android.intent.action.BUGREPORT_FINISHED" /> </intent-filter> </receiver> + + <service + android:name=".BugreportProgressService" + android:exported="false"/> </application> </manifest> diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java new file mode 100644 index 000000000000..a2030ef68817 --- /dev/null +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.shell; + +import static com.android.shell.BugreportPrefs.STATE_SHOW; +import static com.android.shell.BugreportPrefs.getWarningState; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import libcore.io.Streams; + +import com.google.android.collect.Lists; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.ClipData; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.IBinder; +import android.os.SystemProperties; +import android.support.v4.content.FileProvider; +import android.util.Log; +import android.util.Patterns; +import android.widget.Toast; + +public class BugreportProgressService extends Service { + private static final String TAG = "Shell"; + + private static final String AUTHORITY = "com.android.shell"; + + static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT"; + static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT"; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null) { + onBugreportFinished(intent); + } + return START_NOT_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void onBugreportFinished(Intent intent) { + final Context context = getApplicationContext(); + final Configuration conf = context.getResources().getConfiguration(); + final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT); + final File screenshotFile = getFileExtra(intent, EXTRA_SCREENSHOT); + + if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) { + triggerLocalNotification(context, bugreportFile, screenshotFile); + } + stopSelf(); + } + + /** + * Responsible for triggering a notification that allows the user to start a + * "share" intent with the bug report. On watches we have other methods to allow the user to + * start this intent (usually by triggering it on another connected device); we don't need to + * display the notification in this case. + */ + private static void triggerLocalNotification(final Context context, final File bugreportFile, + final File screenshotFile) { + if (!bugreportFile.exists() || !bugreportFile.canRead()) { + Log.e(TAG, "Could not read bugreport file " + bugreportFile); + Toast.makeText(context, context.getString(R.string.bugreport_unreadable_text), + Toast.LENGTH_LONG).show(); + return; + } + + boolean isPlainText = bugreportFile.getName().toLowerCase().endsWith(".txt"); + if (!isPlainText) { + // Already zipped, send it right away. + sendBugreportNotification(context, bugreportFile, screenshotFile); + } else { + // Asynchronously zip the file first, then send it. + sendZippedBugreportNotification(context, bugreportFile, screenshotFile); + } + } + + private static Intent buildWarningIntent(Context context, Intent sendIntent) { + final Intent intent = new Intent(context, BugreportWarningActivity.class); + intent.putExtra(Intent.EXTRA_INTENT, sendIntent); + return intent; + } + + /** + * Build {@link Intent} that can be used to share the given bugreport. + */ + private static Intent buildSendIntent(Context context, Uri bugreportUri, Uri screenshotUri) { + final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); + final String mimeType = "application/vnd.android.bugreport"; + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setType(mimeType); + + intent.putExtra(Intent.EXTRA_SUBJECT, bugreportUri.getLastPathSegment()); + + // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String. + // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually + // create the ClipData object with the attachments URIs. + String messageBody = String.format("Build info: %s\nSerial number:%s", + SystemProperties.get("ro.build.description"), SystemProperties.get("ro.serialno")); + intent.putExtra(Intent.EXTRA_TEXT, messageBody); + final ClipData clipData = new ClipData(null, new String[] { mimeType }, + new ClipData.Item(null, null, null, bugreportUri)); + final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri); + if (screenshotUri != null) { + clipData.addItem(new ClipData.Item(null, null, null, screenshotUri)); + attachments.add(screenshotUri); + } + intent.setClipData(clipData); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments); + + final Account sendToAccount = findSendToAccount(context); + if (sendToAccount != null) { + intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name }); + } + + return intent; + } + + /** + * Sends a bugreport notitication. + */ + private static void sendBugreportNotification(Context context, File bugreportFile, + File screenshotFile) { + // Files are kept on private storage, so turn into Uris that we can + // grant temporary permissions for. + final Uri bugreportUri = getUri(context, bugreportFile); + final Uri screenshotUri = getUri(context, screenshotFile); + + Intent sendIntent = buildSendIntent(context, bugreportUri, screenshotUri); + Intent notifIntent; + + // Send through warning dialog by default + if (getWarningState(context, STATE_SHOW) == STATE_SHOW) { + notifIntent = buildWarningIntent(context, sendIntent); + } else { + notifIntent = sendIntent; + } + notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + final Notification.Builder builder = new Notification.Builder(context) + .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) + .setContentTitle(context.getString(R.string.bugreport_finished_title)) + .setTicker(context.getString(R.string.bugreport_finished_title)) + .setContentText(context.getString(R.string.bugreport_finished_text)) + .setContentIntent(PendingIntent.getActivity( + context, 0, notifIntent, PendingIntent.FLAG_CANCEL_CURRENT)) + .setAutoCancel(true) + .setLocalOnly(true) + .setColor(context.getColor( + com.android.internal.R.color.system_notification_accent_color)); + + NotificationManager.from(context).notify(TAG, 0, builder.build()); + } + + /** + * Sends a zipped bugreport notification. + */ + private static void sendZippedBugreportNotification(final Context context, + final File bugreportFile, final File screenshotFile) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + File zippedFile = zipBugreport(bugreportFile); + sendBugreportNotification(context, zippedFile, screenshotFile); + return null; + } + }.execute(); + } + + /** + * Zips a bugreport file, returning the path to the new file (or to the + * original in case of failure). + */ + private static File zipBugreport(File bugreportFile) { + String bugreportPath = bugreportFile.getAbsolutePath(); + String zippedPath = bugreportPath.replace(".txt", ".zip"); + Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath); + File bugreportZippedFile = new File(zippedPath); + try (InputStream is = new FileInputStream(bugreportFile); + ZipOutputStream zos = new ZipOutputStream( + new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) { + ZipEntry entry = new ZipEntry(bugreportFile.getName()); + entry.setTime(bugreportFile.lastModified()); + zos.putNextEntry(entry); + int totalBytes = Streams.copy(is, zos); + Log.v(TAG, "size of original bugreport: " + totalBytes + " bytes"); + zos.closeEntry(); + // Delete old file; + boolean deleted = bugreportFile.delete(); + if (deleted) { + Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")"); + } else { + Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")"); + } + return bugreportZippedFile; + } catch (IOException e) { + Log.e(TAG, "exception zipping file " + zippedPath, e); + return bugreportFile; // Return original. + } + } + + /** + * Find the best matching {@link Account} based on build properties. + */ + private static Account findSendToAccount(Context context) { + final AccountManager am = (AccountManager) context.getSystemService( + Context.ACCOUNT_SERVICE); + + String preferredDomain = SystemProperties.get("sendbug.preferred.domain"); + if (!preferredDomain.startsWith("@")) { + preferredDomain = "@" + preferredDomain; + } + + final Account[] accounts = am.getAccounts(); + Account foundAccount = null; + for (Account account : accounts) { + if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { + if (!preferredDomain.isEmpty()) { + // if we have a preferred domain and it matches, return; otherwise keep + // looking + if (account.name.endsWith(preferredDomain)) { + return account; + } else { + foundAccount = account; + } + // if we don't have a preferred domain, just return since it looks like + // an email address + } else { + return account; + } + } + } + return foundAccount; + } + + private static Uri getUri(Context context, File file) { + return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null; + } + + static File getFileExtra(Intent intent, String key) { + final String path = intent.getStringExtra(key); + if (path != null) { + return new File(path); + } else { + return null; + } + } +} diff --git a/packages/Shell/src/com/android/shell/BugreportReceiver.java b/packages/Shell/src/com/android/shell/BugreportReceiver.java index c26437285050..f1da14d57c24 100644 --- a/packages/Shell/src/com/android/shell/BugreportReceiver.java +++ b/packages/Shell/src/com/android/shell/BugreportReceiver.java @@ -16,53 +16,23 @@ package com.android.shell; -import static com.android.shell.BugreportPrefs.STATE_SHOW; -import static com.android.shell.BugreportPrefs.getWarningState; +import static com.android.shell.BugreportProgressService.EXTRA_BUGREPORT; +import static com.android.shell.BugreportProgressService.getFileExtra; + +import java.io.File; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.BroadcastReceiver; -import android.content.ClipData; import android.content.Context; import android.content.Intent; -import android.content.res.Configuration; -import android.net.Uri; import android.os.AsyncTask; import android.os.FileUtils; -import android.os.SystemProperties; -import android.support.v4.content.FileProvider; import android.text.format.DateUtils; -import android.util.Log; -import android.util.Patterns; -import android.widget.Toast; - -import com.google.android.collect.Lists; -import libcore.io.Streams; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; -import java.util.ArrayList; /** * Receiver that handles finished bugreports, usually by attaching them to an - * {@link Intent#ACTION_SEND}. + * {@link Intent#ACTION_SEND_MULTIPLE}. */ public class BugreportReceiver extends BroadcastReceiver { - private static final String TAG = "Shell"; - - private static final String AUTHORITY = "com.android.shell"; - - private static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT"; - private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT"; /** * Always keep the newest 8 bugreport files; 4 reports and 4 screenshots are @@ -77,15 +47,17 @@ public class BugreportReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - final Configuration conf = context.getResources().getConfiguration(); - final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT); - final File screenshotFile = getFileExtra(intent, EXTRA_SCREENSHOT); + // Clean up older bugreports in background + cleanupOldFiles(intent); - if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) { - triggerLocalNotification(context, bugreportFile, screenshotFile); - } + // Delegate to service. + Intent serviceIntent = new Intent(context, BugreportProgressService.class); + serviceIntent.putExtras(intent.getExtras()); + context.startService(serviceIntent); + } - // Clean up older bugreports in background + private void cleanupOldFiles(Intent intent) { + final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT); final PendingResult result = goAsync(); new AsyncTask<Void, Void, Void>() { @Override @@ -97,201 +69,4 @@ public class BugreportReceiver extends BroadcastReceiver { } }.execute(); } - - /** - * Responsible for triggering a notification that allows the user to start a - * "share" intent with the bug report. On watches we have other methods to allow the user to - * start this intent (usually by triggering it on another connected device); we don't need to - * display the notification in this case. - */ - private void triggerLocalNotification(final Context context, final File bugreportFile, - final File screenshotFile) { - if (!bugreportFile.exists() || !bugreportFile.canRead()) { - Log.e(TAG, "Could not read bugreport file " + bugreportFile); - Toast.makeText(context, context.getString(R.string.bugreport_unreadable_text), - Toast.LENGTH_LONG).show(); - return; - } - - boolean isPlainText = bugreportFile.getName().toLowerCase().endsWith(".txt"); - if (!isPlainText) { - // Already zipped, send it right away. - sendBugreportNotification(context, bugreportFile, screenshotFile); - } else { - // Asynchronously zip the file first, then send it. - sendZippedBugreportNotification(context, bugreportFile, screenshotFile); - } - } - - private static Intent buildWarningIntent(Context context, Intent sendIntent) { - final Intent intent = new Intent(context, BugreportWarningActivity.class); - intent.putExtra(Intent.EXTRA_INTENT, sendIntent); - return intent; - } - - /** - * Build {@link Intent} that can be used to share the given bugreport. - */ - private static Intent buildSendIntent(Context context, Uri bugreportUri, Uri screenshotUri) { - final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); - final String mimeType = "application/vnd.android.bugreport"; - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setType(mimeType); - - intent.putExtra(Intent.EXTRA_SUBJECT, bugreportUri.getLastPathSegment()); - - // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String. - // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually - // create the ClipData object with the attachments URIs. - String messageBody = String.format("Build info: %s\nSerial number:%s", - SystemProperties.get("ro.build.description"), SystemProperties.get("ro.serialno")); - intent.putExtra(Intent.EXTRA_TEXT, messageBody); - final ClipData clipData = new ClipData(null, new String[] { mimeType }, - new ClipData.Item(null, null, null, bugreportUri)); - final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri); - if (screenshotUri != null) { - clipData.addItem(new ClipData.Item(null, null, null, screenshotUri)); - attachments.add(screenshotUri); - } - intent.setClipData(clipData); - intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments); - - final Account sendToAccount = findSendToAccount(context); - if (sendToAccount != null) { - intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name }); - } - - return intent; - } - - /** - * Sends a bugreport notitication. - */ - private static void sendBugreportNotification(Context context, File bugreportFile, - File screenshotFile) { - // Files are kept on private storage, so turn into Uris that we can - // grant temporary permissions for. - final Uri bugreportUri = getUri(context, bugreportFile); - final Uri screenshotUri = getUri(context, screenshotFile); - - Intent sendIntent = buildSendIntent(context, bugreportUri, screenshotUri); - Intent notifIntent; - - // Send through warning dialog by default - if (getWarningState(context, STATE_SHOW) == STATE_SHOW) { - notifIntent = buildWarningIntent(context, sendIntent); - } else { - notifIntent = sendIntent; - } - notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - final Notification.Builder builder = new Notification.Builder(context) - .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) - .setContentTitle(context.getString(R.string.bugreport_finished_title)) - .setTicker(context.getString(R.string.bugreport_finished_title)) - .setContentText(context.getString(R.string.bugreport_finished_text)) - .setContentIntent(PendingIntent.getActivity( - context, 0, notifIntent, PendingIntent.FLAG_CANCEL_CURRENT)) - .setAutoCancel(true) - .setLocalOnly(true) - .setColor(context.getColor( - com.android.internal.R.color.system_notification_accent_color)); - - NotificationManager.from(context).notify(TAG, 0, builder.build()); - } - - /** - * Sends a zipped bugreport notification. - */ - private static void sendZippedBugreportNotification(final Context context, - final File bugreportFile, final File screenshotFile) { - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - File zippedFile = zipBugreport(bugreportFile); - sendBugreportNotification(context, zippedFile, screenshotFile); - return null; - } - }.execute(); - } - - /** - * Zips a bugreport file, returning the path to the new file (or to the - * original in case of failure). - */ - private static File zipBugreport(File bugreportFile) { - String bugreportPath = bugreportFile.getAbsolutePath(); - String zippedPath = bugreportPath.replace(".txt", ".zip"); - Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath); - File bugreportZippedFile = new File(zippedPath); - try (InputStream is = new FileInputStream(bugreportFile); - ZipOutputStream zos = new ZipOutputStream( - new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) { - ZipEntry entry = new ZipEntry(bugreportFile.getName()); - entry.setTime(bugreportFile.lastModified()); - zos.putNextEntry(entry); - int totalBytes = Streams.copy(is, zos); - Log.v(TAG, "size of original bugreport: " + totalBytes + " bytes"); - zos.closeEntry(); - // Delete old file; - boolean deleted = bugreportFile.delete(); - if (deleted) { - Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")"); - } else { - Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")"); - } - return bugreportZippedFile; - } catch (IOException e) { - Log.e(TAG, "exception zipping file " + zippedPath, e); - return bugreportFile; // Return original. - } - } - - /** - * Find the best matching {@link Account} based on build properties. - */ - private static Account findSendToAccount(Context context) { - final AccountManager am = (AccountManager) context.getSystemService( - Context.ACCOUNT_SERVICE); - - String preferredDomain = SystemProperties.get("sendbug.preferred.domain"); - if (!preferredDomain.startsWith("@")) { - preferredDomain = "@" + preferredDomain; - } - - final Account[] accounts = am.getAccounts(); - Account foundAccount = null; - for (Account account : accounts) { - if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { - if (!preferredDomain.isEmpty()) { - // if we have a preferred domain and it matches, return; otherwise keep - // looking - if (account.name.endsWith(preferredDomain)) { - return account; - } else { - foundAccount = account; - } - // if we don't have a preferred domain, just return since it looks like - // an email address - } else { - return account; - } - } - } - return foundAccount; - } - - private static Uri getUri(Context context, File file) { - return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null; - } - - private static File getFileExtra(Intent intent, String key) { - final String path = intent.getStringExtra(key); - if (path != null) { - return new File(path); - } else { - return null; - } - } } diff --git a/packages/Shell/tests/Android.mk b/packages/Shell/tests/Android.mk new file mode 100644 index 000000000000..62a37bc70f4e --- /dev/null +++ b/packages/Shell/tests/Android.mk @@ -0,0 +1,20 @@ + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_JAVA_LIBRARIES := android.test.runner + +# TODO: update and/or remove +LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator +#LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 mockito-target ub-uiautomator + +LOCAL_PACKAGE_NAME := ShellTests +LOCAL_INSTRUMENTATION_FOR := Shell + +LOCAL_CERTIFICATE := platform + +include $(BUILD_PACKAGE) diff --git a/packages/Shell/tests/AndroidManifest.xml b/packages/Shell/tests/AndroidManifest.xml new file mode 100644 index 000000000000..54b0802e1504 --- /dev/null +++ b/packages/Shell/tests/AndroidManifest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.shell.tests"> + + <application> + <uses-library android:name="android.test.runner" /> + + <activity + android:name="com.android.shell.ActionSendMultipleConsumerActivity" + android:label="ActionSendMultipleConsumer" + android:theme="@android:style/Theme.NoDisplay" + android:noHistory="true" + android:excludeFromRecents="true"> + <intent-filter> + <action android:name="android.intent.action.SEND_MULTIPLE" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:mimeType="*/*" /> + </intent-filter> + </activity> + </application> + + <instrumentation android:name="android.test.InstrumentationTestRunner" + android:targetPackage="com.android.shell" + android:label="Tests for Shell" /> + +</manifest> diff --git a/packages/Shell/tests/src/com/android/shell/ActionSendMultipleConsumerActivity.java b/packages/Shell/tests/src/com/android/shell/ActionSendMultipleConsumerActivity.java new file mode 100644 index 000000000000..e3e99b0d5711 --- /dev/null +++ b/packages/Shell/tests/src/com/android/shell/ActionSendMultipleConsumerActivity.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.shell; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; + +/** + * Activity responsible for handling ACTION_SEND_MULTIPLE intents and passing them back to the test + * case class (through a {@link CustomActionSendMultipleListener}). + */ +public class ActionSendMultipleConsumerActivity extends Activity { + + private static final String CUSTOM_ACTION_SEND_MULTIPLE_INTENT = + "com.android.shell.tests.CUSTOM_ACTION_SEND_MULTIPLE"; + + private static CustomActionSendMultipleListener sListener; + + static final String UI_NAME = "ActionSendMultipleConsumer"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // The original intent cannot be broadcasted, it will fail due to security violations. + // Since the test case is only interested in the extras, we need to create a new custom + // intent with just them. + final Intent intent = getIntent(); + final Intent customIntent = new Intent(CUSTOM_ACTION_SEND_MULTIPLE_INTENT); + customIntent.putExtras(intent.getExtras()); + + getApplicationContext().sendBroadcast(customIntent); + } + + @Override + protected void onResume() { + super.onResume(); + /* + * TODO: if finish() is not called, app will crash with an exception such as: + * AndroidRuntime: java.lang.RuntimeException: Unable to resume activity + * {com.android.shell.tests/com.android.shell.SendMultipleActivity}: + * java.lang.IllegalStateException: Activity + * {com.android.shell.tests/com.android.shell.SendMultipleActivity} did not call finish() + * prior to onResume() completing. That seems to be a problem on M: + * https://code.google.com/p/android-developer-preview/issues/detail?id=2353 + */ + finish(); + } + + /** + * Gets the {@link CustomActionSendMultipleListener} singleton. + */ + static CustomActionSendMultipleListener getListener(Context context) { + synchronized (ActionSendMultipleConsumerActivity.class) { + if (sListener == null) { + sListener = new CustomActionSendMultipleListener(context); + } + } + return sListener; + } + + /** + * Listener of custom ACTION_SEND_MULTIPLE_INTENTS. + */ + static class CustomActionSendMultipleListener { + + private static final int TIMEOUT = 10; + private final BlockingQueue<Bundle> mQueue = new SynchronousQueue<>(); + + public CustomActionSendMultipleListener(Context context) { + BroadcastReceiver receiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + try { + mQueue.put(intent.getExtras()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }; + + final IntentFilter filter = new IntentFilter(); + filter.addAction(CUSTOM_ACTION_SEND_MULTIPLE_INTENT); + context.registerReceiver(receiver, filter); + } + + /** + * Gets the extras from the custom intent, blocking until it's received. + */ + Bundle getExtras() { + Bundle bundle = null; + try { + bundle = mQueue.poll(TIMEOUT, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + if (bundle == null) { + throw new IllegalStateException("Intent not received after " + TIMEOUT + "s"); + } + return bundle; + } + } +} diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java new file mode 100644 index 000000000000..1bdd9ddc874d --- /dev/null +++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.shell; + +import static android.test.MoreAsserts.assertContainsRegex; +import static com.android.shell.ActionSendMultipleConsumerActivity.UI_NAME; +import static com.android.shell.BugreportProgressService.EXTRA_BUGREPORT; +import static com.android.shell.BugreportProgressService.EXTRA_SCREENSHOT; + +import java.io.BufferedOutputStream; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import libcore.io.Streams; +import android.app.Instrumentation; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.SystemProperties; +import android.service.notification.StatusBarNotification; +import android.support.test.uiautomator.UiDevice; +import android.test.InstrumentationTestCase; +import android.util.Log; + +import com.android.shell.ActionSendMultipleConsumerActivity.CustomActionSendMultipleListener; + +/** + * Integration tests for {@link BugreportReceiver}. + * <p> + * These tests don't mock any component and rely on external UI components (like the notification + * bar and activity chooser), which can make them unreliable and slow. + * <p> + * The general workflow is: + * <ul> + * <li>creates the bug report files + * <li>generates the BUGREPORT_FINISHED intent + * <li>emulate user actions to share the intent with a custom activity + * <li>asserts the extras received by the custom activity + * </ul> + * <p> + * TODO: currently, these tests only work if the bug report sharing warning is disabled and the + * device screen is unlocked. + */ +public class BugreportReceiverTest extends InstrumentationTestCase { + + private static final String TAG = "BugreportReceiverTest"; + + // Timeout for UI operations, in milliseconds. + private static final int TIMEOUT = 1000; + + private static final String ROOT_DIR = "/data/data/com.android.shell/files/bugreports"; + private static final String BUGREPORT_FILE = "test_bugreport.txt"; + private static final String ZIP_FILE = "test_bugreport.zip"; + private static final String PLAIN_TEXT_PATH = ROOT_DIR + "/" + BUGREPORT_FILE; + private static final String ZIP_PATH = ROOT_DIR + "/" + ZIP_FILE; + private static final String SCREENSHOT_PATH = ROOT_DIR + "/test_screenshot.png"; + + private static final String BUGREPORT_CONTENT = "Dump, might as well dump!\n"; + private static final String SCREENSHOT_CONTENT = "A picture is worth a thousand words!\n"; + + private Context mContext; + private UiBot mUiBot; + private CustomActionSendMultipleListener mListener; + + @Override + protected void setUp() throws Exception { + Instrumentation instrumentation = getInstrumentation(); + mContext = instrumentation.getTargetContext(); + mUiBot = new UiBot(UiDevice.getInstance(instrumentation), TIMEOUT); + mListener = ActionSendMultipleConsumerActivity.getListener(mContext); + cancelExistingNotifications(); + } + + public void testBugreportFinished_plainBugreportAndScreenshot() throws Exception { + createTextFile(PLAIN_TEXT_PATH, BUGREPORT_CONTENT); + createTextFile(SCREENSHOT_PATH, SCREENSHOT_CONTENT); + Bundle extras = sendBugreportFinishedIntent(PLAIN_TEXT_PATH, SCREENSHOT_PATH); + assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT); + } + + public void testBugreportFinished_zippedBugreportAndScreenshot() throws Exception { + createZipFile(ZIP_PATH, BUGREPORT_FILE, BUGREPORT_CONTENT); + createTextFile(SCREENSHOT_PATH, SCREENSHOT_CONTENT); + Bundle extras = sendBugreportFinishedIntent(ZIP_PATH, SCREENSHOT_PATH); + assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT); + } + + public void testBugreportFinished_plainBugreportAndNoScreenshot() throws Exception { + createTextFile(PLAIN_TEXT_PATH, BUGREPORT_CONTENT); + Bundle extras = sendBugreportFinishedIntent(PLAIN_TEXT_PATH, null); + assertActionSendMultiple(extras, BUGREPORT_CONTENT, null); + } + + public void testBugreportFinished_zippedBugreportAndNoScreenshot() throws Exception { + createZipFile(ZIP_PATH, BUGREPORT_FILE, BUGREPORT_CONTENT); + Bundle extras = sendBugreportFinishedIntent(ZIP_PATH, null); + assertActionSendMultiple(extras, BUGREPORT_CONTENT, null); + } + + private void cancelExistingNotifications() { + NotificationManager nm = NotificationManager.from(mContext); + for (StatusBarNotification notification : nm.getActiveNotifications()) { + int id = notification.getId(); + Log.i(TAG, "Canceling existing notification (id=" + id + ")"); + nm.cancel(id); + } + } + + /** + * Sends a "bugreport finished" intent and waits for the result. + * + * @return extras sent to the bugreport finished consumer. + */ + private Bundle sendBugreportFinishedIntent(String bugreportPath, String screenshotPath) { + Intent intent = new Intent("android.intent.action.BUGREPORT_FINISHED"); + if (bugreportPath != null) { + intent.putExtra(EXTRA_BUGREPORT, bugreportPath); + } + if (screenshotPath != null) { + intent.putExtra(EXTRA_SCREENSHOT, screenshotPath); + } + + mContext.sendBroadcast(intent); + + mUiBot.clickOnNotification(mContext.getString(R.string.bugreport_finished_title)); + mUiBot.chooseActivity(UI_NAME); + return mListener.getExtras(); + } + + /** + * Asserts the proper ACTION_SEND_MULTIPLE intent was sent. + */ + private void assertActionSendMultiple(Bundle extras, String bugreportContent, + String screenshotContent) throws IOException { + String body = extras.getString(Intent.EXTRA_TEXT); + assertContainsRegex("missing build info", + SystemProperties.get("ro.build.description"), body); + assertContainsRegex("missing serial number", + SystemProperties.get("ro.serialno"), body); + + assertEquals("wrong subject", ZIP_FILE, extras.getString(Intent.EXTRA_SUBJECT)); + + List<Uri> attachments = extras.getParcelableArrayList(Intent.EXTRA_STREAM); + int expectedSize = screenshotContent != null ? 2 : 1; + assertEquals("wrong number of attachments", expectedSize, attachments.size()); + + // Need to interact through all attachments, since order is not guaranteed. + Uri zipUri = null, screenshotUri = null; + for (Uri attachment : attachments) { + if (attachment.getPath().endsWith(".zip")) { + zipUri = attachment; + } + if (attachment.getPath().endsWith(".png")) { + screenshotUri = attachment; + } + } + assertNotNull("did not get .zip attachment", zipUri); + assertZipContent(zipUri, BUGREPORT_FILE, BUGREPORT_CONTENT); + + if (screenshotContent != null) { + assertNotNull("did not get .png attachment", screenshotUri); + assertContent(screenshotUri, SCREENSHOT_CONTENT); + } else { + assertNull("should not have .png attachment", screenshotUri); + } + } + + private void assertContent(Uri uri, String expectedContent) throws IOException { + Log.v(TAG, "assertContents(uri=" + uri); + try (InputStream is = mContext.getContentResolver().openInputStream(uri)) { + String actualContent = new String(Streams.readFully(is)); + assertEquals("wrong content for '" + uri + "'", expectedContent, actualContent); + } + } + + private void assertZipContent(Uri uri, String entryName, String expectedContent) + throws IOException, IOException { + Log.v(TAG, "assertZipEntry(uri=" + uri + ", entryName=" + entryName); + try (ZipInputStream zis = new ZipInputStream(mContext.getContentResolver().openInputStream( + uri))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + Log.v(TAG, "Zip entry: " + entry.getName()); + if (entry.getName().equals(entryName)) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + Streams.copy(zis, bos); + String actualContent = new String(bos.toByteArray(), "UTF-8"); + bos.close(); + assertEquals("wrong content for zip entry'" + entryName + "' on '" + uri + "'", + expectedContent, actualContent); + return; + } + } + } + fail("Did not find entry '" + entryName + "' on file '" + uri + "'"); + } + + private static void createTextFile(String path, String content) throws IOException { + Log.v(TAG, "createFile(" + path + ")"); + try (Writer writer = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(path)))) { + writer.write(content); + } + } + + private void createZipFile(String path, String entryName, String content) throws IOException { + Log.v(TAG, "createZipFile(" + path + ", " + entryName + ")"); + try (ZipOutputStream zos = new ZipOutputStream( + new BufferedOutputStream(new FileOutputStream(path)))) { + ZipEntry entry = new ZipEntry(entryName); + zos.putNextEntry(entry); + byte[] data = content.getBytes(); + zos.write(data, 0, data.length); + zos.closeEntry(); + } + } +} diff --git a/packages/Shell/tests/src/com/android/shell/UiBot.java b/packages/Shell/tests/src/com/android/shell/UiBot.java new file mode 100644 index 000000000000..f5dd31c6fdc0 --- /dev/null +++ b/packages/Shell/tests/src/com/android/shell/UiBot.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.shell; + +import android.support.test.uiautomator.By; +import android.support.test.uiautomator.UiDevice; +import android.support.test.uiautomator.UiObject; +import android.support.test.uiautomator.UiObjectNotFoundException; +import android.support.test.uiautomator.UiSelector; +import android.support.test.uiautomator.Until; +import android.util.Log; +import static junit.framework.Assert.assertTrue; + +/** + * A helper class for UI-related testing tasks. + */ +final class UiBot { + + private static final String TAG = "UiBot"; + private static final String SYSTEMUI_PACKAGED = "com.android.systemui"; + + private final UiDevice mDevice; + private final int mTimeout; + + public UiBot(UiDevice device, int timeout) { + mDevice = device; + mTimeout = timeout; + } + + /** + * Opens the system notification and clicks a given notification. + * + * @param text Notificaton's text as displayed by the UI. + */ + public void clickOnNotification(String text) { + boolean opened = mDevice.openNotification(); + Log.v(TAG, "openNotification(): " + opened); + boolean gotIt = mDevice.wait(Until.hasObject(By.pkg(SYSTEMUI_PACKAGED)), mTimeout); + assertTrue("could not get system ui (" + SYSTEMUI_PACKAGED + ")", gotIt); + + gotIt = mDevice.wait(Until.hasObject(By.text(text)), mTimeout); + assertTrue("object with text '(" + text + "') not visible yet", gotIt); + + UiObject notification = getVisibleObject(text); + + click(notification, "bug report notification"); + } + + /** + * Gets an object which is guaranteed to be present in the current UI.\ + * + * @param text Object's text as displayed by the UI. + */ + public UiObject getVisibleObject(String text) { + UiObject uiObject = mDevice.findObject(new UiSelector().text(text)); + assertTrue("could not find object with text '(" + text + "')", uiObject.exists()); + return uiObject; + } + + /** + * Clicks on a UI element. + * + * @param uiObject UI element to be clicked. + * @param description Elements's description used on logging statements. + */ + public void click(UiObject uiObject, String description) { + try { + boolean clicked = uiObject.click(); + // TODO: assertion below fails sometimes, even though the click succeeded, + // (specially when clicking the "Just Once" button), so it's currently just logged. + // assertTrue("could not click on object '" + description + "'", clicked); + + Log.v(TAG, "onClick for " + description + ": " + clicked); + } catch (UiObjectNotFoundException e) { + throw new IllegalStateException("exception when clicking on object '" + description + + "'", e); + } + } + + /** + * Chooses a given activity to handle an Intent, using the "Just Once" button. + * + * @param name name of the activity as displayed in the UI (typically the value set by + * {@code android:label} in the manifest). + */ + // TODO: UI Automator should provide such logic. + public void chooseActivity(String name) { + // First select activity if it's not the default option. + boolean gotIt = mDevice.wait(Until.hasObject(By.text(name)), mTimeout); + // TODO: if the activity is indeed the default option, call above will timeout, which will + // make the tests run slower. It might be better to change the logic to assume the default + // first. + if (gotIt) { + Log.v(TAG, "Found activity " + name + ", it's not default action"); + UiObject activityChooser = getVisibleObject(name); + click(activityChooser, "activity chooser"); + } else { + String text = String.format("Share with %s", name); + Log.v(TAG, "Didn't find activity " + name + + ", assuming it's the default action and search for '" + text + "'"); + gotIt = mDevice.wait(Until.hasObject(By.text(text)), mTimeout); + assertTrue("did not find text '" + text + "'", gotIt); + } + + // Then clicks the "Just Once" button. + gotIt = mDevice + .wait(Until.hasObject(By.res("android", "button_once")), mTimeout); + assertTrue("'Just Once' button not visible yet", gotIt); + + UiObject justOnce = mDevice + .findObject(new UiSelector().resourceId("android:id/button_once")); + assertTrue("'Just Once' button not found", justOnce.exists()); + + click(justOnce, "Just Once"); + } +} diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java index 5e4f2b2be258..78a4e359ad24 100644 --- a/services/core/java/com/android/server/InputMethodManagerService.java +++ b/services/core/java/com/android/server/InputMethodManagerService.java @@ -1225,7 +1225,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub void unbindCurrentClientLocked( /* @InputMethodClient.UnbindReason */ final int unbindClientReason) { if (mCurClient != null) { - if (DEBUG) Slog.v(TAG, "unbindCurrentInputLocked: client = " + if (DEBUG) Slog.v(TAG, "unbindCurrentInputLocked: client=" + mCurClient.client.asBinder()); if (mBoundToMethod) { mBoundToMethod = false; @@ -1290,8 +1290,10 @@ public class InputMethodManagerService extends IInputMethodManager.Stub mCurId, mCurSeq, mCurUserActionNotificationSequenceNumber); } - InputBindResult startInputLocked(IInputMethodClient client, - IInputContext inputContext, EditorInfo attribute, int controlFlags) { + InputBindResult startInputLocked( + /* @InputMethodClient.StartInputReason */ final int startInputReason, + IInputMethodClient client, IInputContext inputContext, EditorInfo attribute, + int controlFlags) { // If no method is currently selected, do nothing. if (mCurMethodId == null) { return mNoBinding; @@ -1320,8 +1322,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub return startInputUncheckedLocked(cs, inputContext, attribute, controlFlags); } - InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, - IInputContext inputContext, @NonNull EditorInfo attribute, int controlFlags) { + InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IInputContext inputContext, + @NonNull EditorInfo attribute, int controlFlags) { // If no method is currently selected, do nothing. if (mCurMethodId == null) { return mNoBinding; @@ -1340,7 +1342,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub // If the client is changing, we need to switch over to the new // one. unbindCurrentClientLocked(InputMethodClient.UNBIND_REASON_SWITCH_CLIENT); - if (DEBUG) Slog.v(TAG, "switching to client: client = " + if (DEBUG) Slog.v(TAG, "switching to client: client=" + cs.client.asBinder() + " keyguard=" + mCurClientInKeyguard); // If the screen is on, inform the new client it is active @@ -1442,15 +1444,26 @@ public class InputMethodManagerService extends IInputMethodManager.Stub } @Override - public InputBindResult startInput(IInputMethodClient client, - IInputContext inputContext, EditorInfo attribute, int controlFlags) { + public InputBindResult startInput( + /* @InputMethodClient.StartInputReason */ final int startInputReason, + IInputMethodClient client, IInputContext inputContext, EditorInfo attribute, + int controlFlags) { if (!calledFromValidUser()) { return null; } synchronized (mMethodMap) { + if (DEBUG) { + Slog.v(TAG, "startInput: reason=" + + InputMethodClient.getStartInputReason(startInputReason) + + " client = " + client.asBinder() + + " inputContext=" + inputContext + + " attribute=" + attribute + + " controlFlags=#" + Integer.toHexString(controlFlags)); + } final long ident = Binder.clearCallingIdentity(); try { - return startInputLocked(client, inputContext, attribute, controlFlags); + return startInputLocked(startInputReason, client, inputContext, attribute, + controlFlags); } finally { Binder.restoreCallingIdentity(ident); } @@ -2151,17 +2164,21 @@ public class InputMethodManagerService extends IInputMethodManager.Stub } @Override - public InputBindResult windowGainedFocus(IInputMethodClient client, IBinder windowToken, - int controlFlags, int softInputMode, int windowFlags, - EditorInfo attribute, IInputContext inputContext) { + public InputBindResult windowGainedFocus( + /* @InputMethodClient.StartInputReason */ final int startInputReason, + IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode, + int windowFlags, EditorInfo attribute, IInputContext inputContext) { // Needs to check the validity before clearing calling identity final boolean calledFromValidUser = calledFromValidUser(); - InputBindResult res = null; long ident = Binder.clearCallingIdentity(); try { synchronized (mMethodMap) { - if (DEBUG) Slog.v(TAG, "windowGainedFocus: " + client.asBinder() + if (DEBUG) Slog.v(TAG, "windowGainedFocus: reason=" + + InputMethodClient.getStartInputReason(startInputReason) + + " client=" + client.asBinder() + + " inputContext=" + inputContext + + " attribute=" + attribute + " controlFlags=#" + Integer.toHexString(controlFlags) + " softInputMode=#" + Integer.toHexString(softInputMode) + " windowFlags=#" + Integer.toHexString(windowFlags)); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index ff829ff321d5..424b90234ca2 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -24,7 +24,6 @@ import android.app.ActivityManagerInternal; import android.app.ActivityManagerNative; import android.app.IActivityManager; import android.app.IStopUserCallback; -import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -216,14 +215,15 @@ public class UserManagerService extends IUserManager.Stub { private final SparseArray<Bundle> mAppliedUserRestrictions = new SparseArray<>(); /** - * User restrictions set by {@link DevicePolicyManager} that should be applied to all users, - * including guests. + * User restrictions set by {@link com.android.server.devicepolicy.DevicePolicyManagerService} + * that should be applied to all users, including guests. */ @GuardedBy("mRestrictionsLock") private Bundle mDevicePolicyGlobalUserRestrictions; /** - * User restrictions set by {@link DevicePolicyManager} for each user. + * User restrictions set by {@link com.android.server.devicepolicy.DevicePolicyManagerService} + * for each user. */ @GuardedBy("mRestrictionsLock") private final SparseArray<Bundle> mDevicePolicyLocalUserRestrictions = new SparseArray<>(); @@ -248,6 +248,12 @@ public class UserManagerService extends IUserManager.Stub { private final LocalService mLocalService; + @GuardedBy("mUsersLock") + private boolean mIsDeviceManaged; + + @GuardedBy("mUsersLock") + private final SparseBooleanArray mIsUserManaged = new SparseBooleanArray(); + @GuardedBy("mUserRestrictionsListeners") private final ArrayList<UserRestrictionsListener> mUserRestrictionsListeners = new ArrayList<>(); @@ -509,11 +515,9 @@ public class UserManagerService extends IUserManager.Stub { if (!userInfo.isAdmin()) { return false; } + // restricted profile can be created if there is no DO set and the admin user has no PO; + return !mIsDeviceManaged && !mIsUserManaged.get(userId); } - DevicePolicyManager dpm = (DevicePolicyManager) mContext.getSystemService( - Context.DEVICE_POLICY_SERVICE); - // restricted profile can be created if there is no DO set and the admin user has no PO - return !dpm.isDeviceManaged() && dpm.getProfileOwnerAsUser(userId) == null; } /* @@ -1596,11 +1600,10 @@ public class UserManagerService extends IUserManager.Stub { if (UserManager.isSplitSystemUser() && !isGuest && !isManagedProfile && getPrimaryUser() == null) { flags |= UserInfo.FLAG_PRIMARY; - DevicePolicyManager devicePolicyManager = (DevicePolicyManager) - mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); - if (devicePolicyManager == null - || !devicePolicyManager.isDeviceManaged()) { - flags |= UserInfo.FLAG_ADMIN; + synchronized (mUsersLock) { + if (!mIsDeviceManaged) { + flags |= UserInfo.FLAG_ADMIN; + } } } userId = getNextAvailableId(); @@ -1865,6 +1868,13 @@ public class UserManagerService extends IUserManager.Stub { // Remove this user from the list synchronized (mUsersLock) { mUsers.remove(userHandle); + mIsUserManaged.delete(userHandle); + } + synchronized (mRestrictionsLock) { + mBaseUserRestrictions.remove(userHandle); + mAppliedUserRestrictions.remove(userHandle); + mCachedEffectiveUserRestrictions.remove(userHandle); + mDevicePolicyLocalUserRestrictions.remove(userHandle); } // Remove user file AtomicFile userFile = new AtomicFile(new File(mUsersDir, userHandle + XML_SUFFIX)); @@ -2376,9 +2386,10 @@ public class UserManagerService extends IUserManager.Stub { if (user == null) { continue; } + final int userId = user.id; pw.print(" "); pw.print(user); pw.print(" serialNo="); pw.print(user.serialNumber); - if (mRemovingUserIds.get(mUsers.keyAt(i))) { + if (mRemovingUserIds.get(userId)) { pw.print(" <removing> "); } if (user.partial) { @@ -2403,6 +2414,8 @@ public class UserManagerService extends IUserManager.Stub { sb.append(" ago"); pw.println(sb); } + pw.print(" Has profile owner: "); + pw.println(mIsUserManaged.get(userId)); pw.println(" Restrictions:"); synchronized (mRestrictionsLock) { UserRestrictionsUtils.dumpRestrictions( @@ -2427,6 +2440,10 @@ public class UserManagerService extends IUserManager.Stub { synchronized (mGuestRestrictions) { UserRestrictionsUtils.dumpRestrictions(pw, " ", mGuestRestrictions); } + synchronized (mUsersLock) { + pw.println(); + pw.println(" Device managed: " + mIsDeviceManaged); + } } } @@ -2507,6 +2524,20 @@ public class UserManagerService extends IUserManager.Stub { mUserRestrictionsListeners.remove(listener); } } + + @Override + public void setDeviceManaged(boolean isManaged) { + synchronized (mUsersLock) { + mIsDeviceManaged = isManaged; + } + } + + @Override + public void setUserManaged(int userId, boolean isManaged) { + synchronized (mUsersLock) { + mIsUserManaged.put(userId, isManaged); + } + } } private class Shell extends ShellCommand { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 21d3bbaea730..ac90daf97b90 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -110,7 +110,6 @@ import android.util.Log; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; -import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.TimeUtils; import android.util.TypedValue; @@ -406,7 +405,7 @@ public class WindowManagerService extends IWindowManager.Stub /** * Windows whose surface should be destroyed. */ - private final ArrayList<WindowState> mDestroySurface = new ArrayList<>(); + final ArrayList<WindowState> mDestroySurface = new ArrayList<>(); /** * Windows with a preserved surface waiting to be destroyed. These windows @@ -443,11 +442,11 @@ public class WindowManagerService extends IWindowManager.Stub WindowState[] mRebuildTmp = new WindowState[20]; /** - * Stores for each user whether screencapture is disabled for all their windows. + * Stores for each user whether screencapture is disabled * This array is essentially a cache for all userId for * {@link android.app.admin.DevicePolicyManager#getScreenCaptureDisabled} */ - private SparseBooleanArray mScreenCaptureDisabled = new SparseBooleanArray(); + SparseArray<Boolean> mScreenCaptureDisabled = new SparseArray<>(); IInputMethodManager mInputMethodManager; @@ -2109,11 +2108,25 @@ public class WindowManagerService extends IWindowManager.Stub executeAppTransition(); } + /** + * Returns whether screen capture is disabled for all windows of a specific user. + */ + boolean isScreenCaptureDisabledLocked(int userId) { + Boolean disabled = mScreenCaptureDisabled.get(userId); + if (disabled == null) { + return false; + } + return disabled; + } + boolean isSecureLocked(WindowState w) { - if ((w.mAttrs.flags & FLAG_SECURE) != 0) { + if ((w.mAttrs.flags&WindowManager.LayoutParams.FLAG_SECURE) != 0) { + return true; + } + if (isScreenCaptureDisabledLocked(UserHandle.getUserId(w.mOwnerUid))) { return true; } - return mScreenCaptureDisabled.get(UserHandle.getUserId(w.mOwnerUid)); + return false; } /** @@ -2637,10 +2650,8 @@ public class WindowManagerService extends IWindowManager.Stub Slog.i(TAG, "Relayout " + win + ": oldVis=" + oldVisibility + " newVis=" + viewVisibility, stack); } - final AppWindowToken appToken = win.mAppToken; - final boolean visible = viewVisibility == View.VISIBLE - && (appToken == null ? win.mPolicyVisibility : !appToken.clientHidden); - if (visible) { + if (viewVisibility == View.VISIBLE && + (win.mAppToken == null || !win.mAppToken.clientHidden)) { result = relayoutVisibleWindow(outConfig, result, win, winAnimator, attrChanges, oldVisibility); try { @@ -2726,8 +2737,8 @@ public class WindowManagerService extends IWindowManager.Stub mWallpaperControllerLocked.updateWallpaperOffset( win, displayInfo.logicalWidth, displayInfo.logicalHeight, false); } - if (appToken != null) { - appToken.updateReportedVisibilityLocked(); + if (win.mAppToken != null) { + win.mAppToken.updateReportedVisibilityLocked(); } if (winAnimator.mReportSurfaceResized) { winAnimator.mReportSurfaceResized = false; @@ -10200,25 +10211,6 @@ public class WindowManagerService extends IWindowManager.Stub mDestroySurface.add(win); } - boolean destroySurfacesLocked() { - boolean wallpaperDestroyed = false; - for (int i = mDestroySurface.size() - 1; i >= 0; i--) { - WindowState win = mDestroySurface.get(i); - win.mDestroying = false; - if (mInputMethodWindow == win) { - mInputMethodWindow = null; - } - if (mWallpaperControllerLocked.isWallpaperTarget(win)) { - wallpaperDestroyed = true; - } - if (!win.shouldSaveSurface()) { - win.mWinAnimator.destroySurfaceLocked(); - } - } - mDestroySurface.clear(); - return wallpaperDestroyed; - } - private final class LocalService extends WindowManagerInternal { @Override public void requestTraversalFromDisplayManager() { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index a0a2162b0f90..064b4128a2ce 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -411,10 +411,6 @@ final class WindowState implements WindowManagerPolicy.WindowState { final private Rect mTmpRect = new Rect(); - // This window often remains added but hidden, so we want to destroy its surface when it's not - // visible. - private final boolean mDestroySurfaceWhenHidden; - WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token, WindowState attachedWindow, int appOp, int seq, WindowManager.LayoutParams a, int viewVisibility, final DisplayContent displayContent) { @@ -462,7 +458,6 @@ final class WindowState implements WindowManagerPolicy.WindowState { mSubLayer = 0; mInputWindowHandle = null; mWinAnimator = null; - mDestroySurfaceWhenHidden = false; return; } mDeathRecipient = deathRecipient; @@ -561,7 +556,6 @@ final class WindowState implements WindowManagerPolicy.WindowState { mInputWindowHandle = new InputWindowHandle( mAppToken != null ? mAppToken.mInputApplicationHandle : null, this, displayContent.getDisplayId()); - mDestroySurfaceWhenHidden = mAttrs.type == TYPE_DOCK_DIVIDER; } void attach() { @@ -1319,10 +1313,6 @@ final class WindowState implements WindowManagerPolicy.WindowState { mHasSurface = hasSurface; } - boolean shouldDestroySurfaceWhenAnimationFinishes() { - return mExiting || (mDestroySurfaceWhenHidden && !mPolicyVisibilityAfterAnim); - } - private final class DeadWindowEventReceiver extends InputEventReceiver { DeadWindowEventReceiver(InputChannel inputChannel) { super(inputChannel, mService.mH.getLooper()); @@ -1605,11 +1595,6 @@ final class WindowState implements WindowManagerPolicy.WindowState { // Already showing. return false; } - if (!mHasSurface && mDestroySurfaceWhenHidden) { - // This is a window that doesn't retain the surface when it's hidden, so immediately - // when we want to show it again, we need to create the surface for it. - mWinAnimator.createSurfaceLocked(); - } if (DEBUG_VISIBILITY) Slog.v(TAG, "Policy visibility true: " + this); if (doAnimation) { if (DEBUG_VISIBILITY) Slog.v(TAG, "doAnimation: mPolicyVisibility=" @@ -1645,7 +1630,8 @@ final class WindowState implements WindowManagerPolicy.WindowState { doAnimation = false; } } - final boolean current = doAnimation ? mPolicyVisibilityAfterAnim : mPolicyVisibility; + boolean current = doAnimation ? mPolicyVisibilityAfterAnim + : mPolicyVisibility; if (!current) { // Already hiding. return false; @@ -1656,9 +1642,11 @@ final class WindowState implements WindowManagerPolicy.WindowState { doAnimation = false; } } - mPolicyVisibilityAfterAnim = false; - if (!doAnimation) { + if (doAnimation) { + mPolicyVisibilityAfterAnim = false; + } else { if (DEBUG_VISIBILITY) Slog.v(TAG, "Policy visibility false: " + this); + mPolicyVisibilityAfterAnim = false; mPolicyVisibility = false; // Window is no longer visible -- make sure if we were waiting // for it to be displayed before enabling the display, that diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index 9726034d0c0f..93c2ff608442 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -446,7 +446,7 @@ class WindowStateAnimator { } } - if (!mWin.shouldDestroySurfaceWhenAnimationFinishes()) { + if (!mWin.mExiting) { return; } @@ -454,13 +454,12 @@ class WindowStateAnimator { return; } - if (localLOGV) Slog.v(TAG, "Exit animation finished in " + this + ": remove=" - + mWin.mRemoveOnExit); + if (WindowManagerService.localLOGV) Slog.v( + TAG, "Exit animation finished in " + this + + ": remove=" + mWin.mRemoveOnExit); if (mSurfaceController != null && mSurfaceController.hasSurface()) { - mService.scheduleSurfaceDestroy(mWin); - if (mWin.mExiting) { - mWin.mDestroying = true; - } + mService.mDestroySurface.add(mWin); + mWin.mDestroying = true; hide("finishExit"); } mWin.mExiting = false; @@ -646,7 +645,7 @@ class WindowStateAnimator { return null; } - if (localLOGV) { + if (WindowManagerService.localLOGV) { Slog.v(TAG, "Got surface: " + mSurfaceController + ", set left=" + w.mFrame.left + " top=" + w.mFrame.top + ", animLayer=" + mAnimLayer); @@ -667,7 +666,7 @@ class WindowStateAnimator { mAnimLayer); mLastHidden = true; - if (localLOGV) Slog.v( + if (WindowManagerService.localLOGV) Slog.v( TAG, "Created surface " + this); } return mSurfaceController; @@ -974,7 +973,7 @@ class WindowStateAnimator { //Slog.i(TAG, "Not applying alpha transform"); } - if ((DEBUG_SURFACE_TRACE || localLOGV) + if ((DEBUG_SURFACE_TRACE || WindowManagerService.localLOGV) && (mShownAlpha == 1.0 || mShownAlpha == 0.0)) Slog.v( TAG, "computeShownFrameLocked: Animating " + this + " mAlpha=" + mAlpha + " self=" + (selfTransformation ? mTransformation.getAlpha() : "null") @@ -995,7 +994,7 @@ class WindowStateAnimator { return; } - if (localLOGV) Slog.v( + if (WindowManagerService.localLOGV) Slog.v( TAG, "computeShownFrameLocked: " + this + " not attached, mAlpha=" + mAlpha); diff --git a/services/core/java/com/android/server/wm/WindowSurfaceController.java b/services/core/java/com/android/server/wm/WindowSurfaceController.java index b3c23d1a8ab0..f8b8d6ced8d8 100644 --- a/services/core/java/com/android/server/wm/WindowSurfaceController.java +++ b/services/core/java/com/android/server/wm/WindowSurfaceController.java @@ -296,8 +296,10 @@ class WindowSurfaceController { } boolean showRobustlyInTransaction() { - if (SHOW_TRANSACTIONS) logSurface("SHOW (performLayout)", null); - if (DEBUG_VISIBILITY) Slog.v(TAG, "Showing " + this + " during relayout"); + if (SHOW_TRANSACTIONS) logSurface( + "SHOW (performLayout)", null); + if (DEBUG_VISIBILITY) Slog.v(TAG, "Showing " + this + + " during relayout"); if (mHiddenForCrop) { return false; diff --git a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java index 54d18e9cf363..214901991fac 100644 --- a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java +++ b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java @@ -381,7 +381,25 @@ class WindowSurfacePlacer { } // Destroy the surface of any windows that are no longer visible. - final boolean wallpaperDestroyed = mService.destroySurfacesLocked(); + boolean wallpaperDestroyed = false; + i = mService.mDestroySurface.size(); + if (i > 0) { + do { + i--; + WindowState win = mService.mDestroySurface.get(i); + win.mDestroying = false; + if (mService.mInputMethodWindow == win) { + mService.mInputMethodWindow = null; + } + if (mWallpaperControllerLocked.isWallpaperTarget(win)) { + wallpaperDestroyed = true; + } + if (!win.shouldSaveSurface()) { + win.mWinAnimator.destroySurfaceLocked(); + } + } while (i > 0); + mService.mDestroySurface.clear(); + } // Time to remove any exiting tokens? for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index f80a61163cdf..819925080ea3 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -1071,7 +1071,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } Owners newOwners() { - return new Owners(mContext); + return new Owners(mContext, getUserManager(), getUserManagerInternal()); } UserManager getUserManager() { @@ -2150,20 +2150,24 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } private void ensureDeviceOwnerUserStarted() { - if (mOwners.hasDeviceOwner()) { - final int userId = mOwners.getDeviceOwnerUserId(); - if (VERBOSE_LOG) { - Log.v(LOG_TAG, "Starting non-system DO user: " + userId); + final int userId; + synchronized (this) { + if (!mOwners.hasDeviceOwner()) { + return; } - if (userId != UserHandle.USER_SYSTEM) { - try { - mInjector.getIActivityManager().startUserInBackground(userId); + userId = mOwners.getDeviceOwnerUserId(); + } + if (VERBOSE_LOG) { + Log.v(LOG_TAG, "Starting non-system DO user: " + userId); + } + if (userId != UserHandle.USER_SYSTEM) { + try { + mInjector.getIActivityManager().startUserInBackground(userId); - // STOPSHIP Prevent the DO user from being killed. + // STOPSHIP Prevent the DO user from being killed. - } catch (RemoteException e) { - Slog.w(LOG_TAG, "Exception starting user", e); - } + } catch (RemoteException e) { + Slog.w(LOG_TAG, "Exception starting user", e); } } } @@ -4584,7 +4588,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { + " for device owner"); } synchronized (this) { - enforceCanSetDeviceOwner(userId); + enforceCanSetDeviceOwnerLocked(userId); // Shutting down backup manager service permanently. long ident = mInjector.binderClearCallingIdentity(); @@ -4751,7 +4755,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { + " not installed for userId:" + userHandle); } synchronized (this) { - enforceCanSetProfileOwner(userHandle); + enforceCanSetProfileOwnerLocked(userHandle); mOwners.setProfileOwner(who, ownerName, userHandle); mOwners.writeProfileOwner(userHandle); return true; @@ -4953,7 +4957,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { * - SYSTEM_UID * - adb if there are not accounts. */ - private void enforceCanSetProfileOwner(int userHandle) { + private void enforceCanSetProfileOwnerLocked(int userHandle) { UserInfo info = mUserManager.getUserInfo(userHandle); if (info == null) { // User doesn't exist. @@ -4995,7 +4999,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { * The device owner can only be set before the setup phase of the primary user has completed, * except for adb if no accounts or additional users are present on the device. */ - private void enforceCanSetDeviceOwner(int userId) { + private void enforceCanSetDeviceOwnerLocked(int userId) { if (mOwners.hasDeviceOwner()) { throw new IllegalStateException("Trying to set the device owner, but device owner " + "is already set."); @@ -6819,20 +6823,22 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { public boolean isProvisioningAllowed(String action) { final int callingUserId = mInjector.userHandleGetCallingUserId(); if (DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE.equals(action)) { - if (mOwners.hasDeviceOwner()) { - if (!mInjector.userManagerIsSplitSystemUser()) { - // Only split-system-user systems support managed-profiles in combination with - // device-owner. - return false; - } - if (mOwners.getDeviceOwnerUserId() != UserHandle.USER_SYSTEM) { - // Only system device-owner supports managed-profiles. Non-system device-owner - // doesn't. - return false; - } - if (callingUserId == UserHandle.USER_SYSTEM) { - // Managed-profiles cannot be setup on the system user, only regular users. - return false; + synchronized (this) { + if (mOwners.hasDeviceOwner()) { + if (!mInjector.userManagerIsSplitSystemUser()) { + // Only split-system-user systems support managed-profiles in combination with + // device-owner. + return false; + } + if (mOwners.getDeviceOwnerUserId() != UserHandle.USER_SYSTEM) { + // Only system device-owner supports managed-profiles. Non-system device-owner + // doesn't. + return false; + } + if (callingUserId == UserHandle.USER_SYSTEM) { + // Managed-profiles cannot be setup on the system user, only regular users. + return false; + } } } if (getProfileOwner(callingUserId) != null) { @@ -6877,8 +6883,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } private boolean isDeviceOwnerProvisioningAllowed(int callingUserId) { - if (mOwners.hasDeviceOwner()) { - return false; + synchronized (this) { + if (mOwners.hasDeviceOwner()) { + return false; + } } if (getProfileOwner(callingUserId) != null) { return false; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java index 435de7ac3f3d..f7de0b3896f7 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java @@ -23,6 +23,7 @@ import android.content.pm.UserInfo; import android.os.Environment; import android.os.UserHandle; import android.os.UserManager; +import android.os.UserManagerInternal; import android.util.ArrayMap; import android.util.AtomicFile; import android.util.Log; @@ -50,6 +51,9 @@ import libcore.io.IoUtils; /** * Stores and restores state for the Device and Profile owners. By definition there can be * only one device owner, but there may be a profile owner for each user. + * + * <p>This class is not thread safe. (i.e. access to this class must always be synchronized + * in the caller side.) */ class Owners { private static final String TAG = "DevicePolicyManagerService"; @@ -78,8 +82,8 @@ class Owners { private static final String TAG_SYSTEM_UPDATE_POLICY = "system-update-policy"; - private final Context mContext; private final UserManager mUserManager; + private final UserManagerInternal mUserManagerInternal; // Internal state for the device owner package. private OwnerInfo mDeviceOwner; @@ -92,44 +96,48 @@ class Owners { // Local system update policy controllable by device owner. private SystemUpdatePolicy mSystemUpdatePolicy; - public Owners(Context context) { - mContext = context; - mUserManager = context.getSystemService(UserManager.class); + public Owners(Context context, UserManager userManager, + UserManagerInternal userManagerInternal) { + mUserManager = userManager; + mUserManagerInternal = userManagerInternal; } /** * Load configuration from the disk. */ void load() { - synchronized (this) { - // First, try to read from the legacy file. - final File legacy = getLegacyConfigFileWithTestOverride(); + // First, try to read from the legacy file. + final File legacy = getLegacyConfigFileWithTestOverride(); - if (readLegacyOwnerFile(legacy)) { - if (DEBUG) { - Log.d(TAG, "Legacy config file found."); - } + final List<UserInfo> users = mUserManager.getUsers(); - // Legacy file exists, write to new files and remove the legacy one. - writeDeviceOwner(); - for (int userId : getProfileOwnerKeys()) { - writeProfileOwner(userId); - } - if (DEBUG) { - Log.d(TAG, "Deleting legacy config file"); - } - if (!legacy.delete()) { - Slog.e(TAG, "Failed to remove the legacy setting file"); - } - } else { - // No legacy file, read from the new format files. - new DeviceOwnerReadWriter().readFromFileLocked(); + if (readLegacyOwnerFile(legacy)) { + if (DEBUG) { + Log.d(TAG, "Legacy config file found."); + } - final List<UserInfo> users = mUserManager.getUsers(); - for (UserInfo ui : users) { - new ProfileOwnerReadWriter(ui.id).readFromFileLocked(); - } + // Legacy file exists, write to new files and remove the legacy one. + writeDeviceOwner(); + for (int userId : getProfileOwnerKeys()) { + writeProfileOwner(userId); + } + if (DEBUG) { + Log.d(TAG, "Deleting legacy config file"); } + if (!legacy.delete()) { + Slog.e(TAG, "Failed to remove the legacy setting file"); + } + } else { + // No legacy file, read from the new format files. + new DeviceOwnerReadWriter().readFromFileLocked(); + + for (UserInfo ui : users) { + new ProfileOwnerReadWriter(ui.id).readFromFileLocked(); + } + } + mUserManagerInternal.setDeviceManaged(hasDeviceOwner()); + for (UserInfo ui : users) { + mUserManagerInternal.setUserManaged(ui.id, hasProfileOwner(ui.id)); } if (hasDeviceOwner() && hasProfileOwner(getDeviceOwnerUserId())) { Slog.w(TAG, String.format("User %d has both DO and PO, which is not supported", @@ -169,21 +177,27 @@ class Owners { boolean userRestrictionsMigrated) { mDeviceOwner = new OwnerInfo(ownerName, admin, userRestrictionsMigrated); mDeviceOwnerUserId = userId; + + mUserManagerInternal.setDeviceManaged(true); } void clearDeviceOwner() { mDeviceOwner = null; mDeviceOwnerUserId = UserHandle.USER_NULL; + + mUserManagerInternal.setDeviceManaged(false); } void setProfileOwner(ComponentName admin, String ownerName, int userId) { // For a newly set PO, there's no need for migration. mProfileOwners.put(userId, new OwnerInfo(ownerName, admin, /* userRestrictionsMigrated =*/ true)); + mUserManagerInternal.setUserManaged(userId, true); } void removeProfileOwner(int userId) { mProfileOwners.remove(userId); + mUserManagerInternal.setUserManaged(userId, false); } ComponentName getProfileOwnerComponent(int userId) { diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java index 56d6fc05d6b5..435b6026830e 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java @@ -52,7 +52,7 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi private final File mProfileOwnerBase; public OwnersTestable(DpmMockContext context) { - super(context); + super(context, context.userManager, context.userManagerInternal); mLegacyFile = new File(context.dataDir, LEGACY_FILE); mDeviceOwnerFile = new File(context.dataDir, DEVICE_OWNER_FILE); mProfileOwnerBase = new File(context.dataDir, PROFILE_OWNER_FILE_BASE); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 32b7383ff9a5..36407e195ac6 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -75,7 +75,7 @@ public class SubscriptionManager { /** * Indicates the caller wants the default phone id. - * Used in SubscriptionController and PhoneBase but do we really need it??? + * Used in SubscriptionController and Phone but do we really need it??? * @hide */ public static final int DEFAULT_PHONE_INDEX = Integer.MAX_VALUE; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java index 8899e53648d2..01c3c500855d 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java @@ -184,8 +184,10 @@ public class BridgeIInputMethodManager implements IInputMethodManager { } @Override - public InputBindResult startInput(IInputMethodClient client, IInputContext inputContext, - EditorInfo attribute, int controlFlags) throws RemoteException { + public InputBindResult startInput( + /* @InputMethodClient.StartInputReason */ int startInputReason, + IInputMethodClient client, IInputContext inputContext, EditorInfo attribute, + int controlFlags) throws RemoteException { // TODO Auto-generated method stub return null; } @@ -226,9 +228,11 @@ public class BridgeIInputMethodManager implements IInputMethodManager { } @Override - public InputBindResult windowGainedFocus(IInputMethodClient client, IBinder windowToken, - int controlFlags, int softInputMode, int windowFlags, EditorInfo attribute, - IInputContext inputContext) throws RemoteException { + public InputBindResult windowGainedFocus( + /* @InputMethodClient.StartInputReason */ int startInputReason, + IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode, + int windowFlags, EditorInfo attribute, IInputContext inputContext) + throws RemoteException { // TODO Auto-generated method stub return null; } |