ActivityMetricsLogger.java revision f970410afef518003c84eef022194848b2a4f606
177d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynskipackage com.android.server.am; 277d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski 377d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynskiimport static android.app.ActivityManager.StackId.DOCKED_STACK_ID; 477d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynskiimport static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID; 577d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynskiimport static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID; 677d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynskiimport static android.app.ActivityManager.StackId.HOME_STACK_ID; 7caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynskiimport static android.app.ActivityManager.StackId.PINNED_STACK_ID; 8f970410afef518003c84eef022194848b2a4f606Jorim Jaggiimport static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; 9f970410afef518003c84eef022194848b2a4f606Jorim Jaggiimport static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; 100e381e278a2c2a3a7c86c9951ac5cbcdc3a186f4Filip Gruszczynskiimport static com.android.server.am.ActivityStack.STACK_INVISIBLE; 1177d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski 12275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggiimport android.annotation.Nullable; 1377d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynskiimport android.app.ActivityManager.StackId; 1477d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynskiimport android.content.Context; 1577d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynskiimport android.os.SystemClock; 16f970410afef518003c84eef022194848b2a4f606Jorim Jaggiimport android.util.Slog; 1777d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski 1877d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynskiimport com.android.internal.logging.MetricsLogger; 19275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggiimport com.android.internal.logging.MetricsProto.MetricsEvent; 2077d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski 2177d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski/** 2277d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski * Handles logging into Tron. 2377d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski */ 2477d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynskiclass ActivityMetricsLogger { 25f970410afef518003c84eef022194848b2a4f606Jorim Jaggi 26f970410afef518003c84eef022194848b2a4f606Jorim Jaggi private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityMetricsLogger" : TAG_AM; 27f970410afef518003c84eef022194848b2a4f606Jorim Jaggi 2877d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski // Window modes we are interested in logging. If we ever introduce a new type, we need to add 2977d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski // a value here and increase the {@link #TRON_WINDOW_STATE_VARZ_STRINGS} array. 3077d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski private static final int WINDOW_STATE_STANDARD = 0; 3177d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski private static final int WINDOW_STATE_SIDE_BY_SIDE = 1; 3277d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski private static final int WINDOW_STATE_FREEFORM = 2; 3377d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski private static final int WINDOW_STATE_INVALID = -1; 3477d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski 35275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi private static final long INVALID_START_TIME = -1; 36275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 3777d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski // Preallocated strings we are sending to tron, so we don't have to allocate a new one every 3877d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski // time we log. 3977d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski private static final String[] TRON_WINDOW_STATE_VARZ_STRINGS = { 40ae255ee61b5e174c2164e58389b8a7ff8a95e14dChris Wren "window_time_0", "window_time_1", "window_time_2"}; 4177d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski 4277d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski private int mWindowState = WINDOW_STATE_STANDARD; 4377d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski private long mLastLogTimeSecs; 4477d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski private final ActivityStackSupervisor mSupervisor; 4577d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski private final Context mContext; 4677d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski 47275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi private long mCurrentTransitionStartTime = INVALID_START_TIME; 48275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi private boolean mLoggedWindowsDrawn; 49275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi private boolean mLoggedStartingWindowDrawn; 50275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi private boolean mLoggedTransitionStarting; 51275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 5277d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski ActivityMetricsLogger(ActivityStackSupervisor supervisor, Context context) { 5377d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski mLastLogTimeSecs = SystemClock.elapsedRealtime() / 1000; 5477d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski mSupervisor = supervisor; 5577d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski mContext = context; 5677d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski } 5777d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski 5877d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski void logWindowState() { 5977d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski final long now = SystemClock.elapsedRealtime() / 1000; 6077d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski if (mWindowState != WINDOW_STATE_INVALID) { 6177d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski // We log even if the window state hasn't changed, because the user might remain in 6277d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski // home/fullscreen move forever and we would like to track this kind of behavior 6377d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski // too. 6477d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski MetricsLogger.count(mContext, TRON_WINDOW_STATE_VARZ_STRINGS[mWindowState], 6577d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski (int) (now - mLastLogTimeSecs)); 6677d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski } 6777d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski mLastLogTimeSecs = now; 6877d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski 6977d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski ActivityStack stack = mSupervisor.getStack(DOCKED_STACK_ID); 708051c5c89060906f5a3a1ca4adb3b53bb423e56bWale Ogunwale if (stack != null && stack.getStackVisibilityLocked(null) != STACK_INVISIBLE) { 7177d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski mWindowState = WINDOW_STATE_SIDE_BY_SIDE; 72caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski return; 73caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski } 74caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski mWindowState = WINDOW_STATE_INVALID; 75caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski stack = mSupervisor.getFocusedStack(); 76caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski if (stack.mStackId == PINNED_STACK_ID) { 77caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski stack = mSupervisor.findStackBehind(stack); 7877d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski } 79caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski if (stack.mStackId == HOME_STACK_ID 80caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski || stack.mStackId == FULLSCREEN_WORKSPACE_STACK_ID) { 81caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski mWindowState = WINDOW_STATE_STANDARD; 82caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski } else if (stack.mStackId == DOCKED_STACK_ID) { 83f970410afef518003c84eef022194848b2a4f606Jorim Jaggi Slog.wtf(TAG, "Docked stack shouldn't be the focused stack, because it reported not" 84f970410afef518003c84eef022194848b2a4f606Jorim Jaggi + " being visible."); 85f970410afef518003c84eef022194848b2a4f606Jorim Jaggi mWindowState = WINDOW_STATE_INVALID; 86caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski } else if (stack.mStackId == FREEFORM_WORKSPACE_STACK_ID) { 87caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski mWindowState = WINDOW_STATE_FREEFORM; 88caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski } else if (StackId.isStaticStack(stack.mStackId)) { 89caae14e478e115d01f9b32890cb31231575e65ddFilip Gruszczynski throw new IllegalStateException("Unknown stack=" + stack); 9077d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski } 9177d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski } 92275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 93275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi /** 94275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * Notifies the tracker at the earliest possible point when we are starting to launch an 95275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * activity. 96275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi */ 97275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi void notifyActivityLaunching() { 98275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi mCurrentTransitionStartTime = System.currentTimeMillis(); 99275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 100275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 101275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi /** 102275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * Notifies the tracker the the activity is actually launching. 103275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * 104275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * @param resultCode one of the ActivityManager.START_* flags, indicating the result of the 105275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * launch 106275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * @param componentName the component name of the activity being launched 107275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * @param processRunning whether the process that will contains the activity is already running 108be67c90f4c2255cab3bc036ecdc8d9636ed5e4b5Jorim Jaggi * @param processSwitch whether the process that will contain the activity didn't have any 109be67c90f4c2255cab3bc036ecdc8d9636ed5e4b5Jorim Jaggi * activity that was stopped, i.e. the started activity is "switching" 110be67c90f4c2255cab3bc036ecdc8d9636ed5e4b5Jorim Jaggi * processes 111275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi */ 112275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi void notifyActivityLaunched(int resultCode, @Nullable String componentName, 113be67c90f4c2255cab3bc036ecdc8d9636ed5e4b5Jorim Jaggi boolean processRunning, boolean processSwitch) { 114275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 115be67c90f4c2255cab3bc036ecdc8d9636ed5e4b5Jorim Jaggi if (resultCode < 0 || componentName == null || !processSwitch) { 116275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 117be67c90f4c2255cab3bc036ecdc8d9636ed5e4b5Jorim Jaggi // Failed to launch or it was not a process switch, so we don't care about the timing. 118275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi reset(); 119275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi return; 120275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 121275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 122275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_COMPONENT_NAME, 123275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi componentName); 124275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_PROCESS_RUNNING, 125275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi processRunning); 126275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_DEVICE_UPTIME_SECONDS, 127275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi (int) (SystemClock.uptimeMillis() / 1000)); 128275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 129275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 130275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi /** 131275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * Notifies the tracker that all windows of the app have been drawn. 132275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi */ 133275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi void notifyWindowsDrawn() { 134275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi if (!isTransitionActive() || mLoggedWindowsDrawn) { 135275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi return; 136275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 137275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_WINDOWS_DRAWN_DELAY_MS, 138275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi calculateCurrentDelay()); 139275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi mLoggedWindowsDrawn = true; 140275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi if (mLoggedTransitionStarting) { 141275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi reset(); 142275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 143275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 144275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 145275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi /** 146275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * Notifies the tracker that the starting window was drawn. 147275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi */ 148275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi void notifyStartingWindowDrawn() { 149275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi if (!isTransitionActive() || mLoggedStartingWindowDrawn) { 150275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi return; 151275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 152275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi mLoggedStartingWindowDrawn = true; 153275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_STARTING_WINDOW_DELAY_MS, 154275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi calculateCurrentDelay()); 155275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 156275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 157275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi /** 158275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * Notifies the tracker that the app transition is starting. 159275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * 160275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * @param reason The reason why we started it. Must be on of 161275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi * ActivityManagerInternal.APP_TRANSITION_* reasons. 162275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi */ 163275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi void notifyTransitionStarting(int reason) { 164275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi if (!isTransitionActive() || mLoggedTransitionStarting) { 165275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi return; 166275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 167275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_REASON, reason); 168275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi MetricsLogger.action(mContext, MetricsEvent.APP_TRANSITION_DELAY_MS, 169275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi calculateCurrentDelay()); 170275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi mLoggedTransitionStarting = true; 171275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi if (mLoggedWindowsDrawn) { 172275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi reset(); 173275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 174275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 175275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 176275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi private boolean isTransitionActive() { 177275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi return mCurrentTransitionStartTime != INVALID_START_TIME; 178275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 179275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 180275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi private void reset() { 181275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi mCurrentTransitionStartTime = INVALID_START_TIME; 182275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi mLoggedWindowsDrawn = false; 183275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi mLoggedTransitionStarting = false; 184275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi mLoggedStartingWindowDrawn = false; 185275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 186275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 187275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi private int calculateCurrentDelay() { 188275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi 189275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi // Shouldn't take more than 25 days to launch an app, so int is fine here. 190275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi return (int) (System.currentTimeMillis() - mCurrentTransitionStartTime); 191275561a74677f9d6c8f3f2cebc3cfea416ca586dJorim Jaggi } 19277d9448e2d6dd8a45c5fedef43c8a1cf4afd28b9Filip Gruszczynski} 193