immersiveSticky and display.screenOriginX

This is a Google bug.  https://code.google.com/p/android/issues/detail?id=170752

It happens to native developers, Unity developers and just about everyone else. We have tried to work around this. If Google is rejecting you because of them, point them to their own bug.  We really can’t do more until Google provides an official solution.

Rob

The “blame the OS” story is just hard to accept, the evidence simply isn’t there, when there are sooo many apps and sooo much evidence that it works fine for the VAST majority of apps (on those same devices where Corona consistently fails)

Your engineers should carefully study the history of that bug report.

I assume they’ve seen similar reports of “real” bugs where hundreds of ppl complain day and night?

This bug report had A SINGLE REPORTER (dan) from April thru October.  And note that the whopping four people (jure,mark,landon,alex) that have contributed since then, all appear to be “home grown” or indie-type developers (no big name corporate email accounts or sigs present).  No offense to those individuals, but isn’t it is just as likely that they simply all made the same coding error?

If you want to perhaps blame Google’s poor developer documentation for “fooling” these 5 devs with obscure implementation details, then that’d be an easy target.  I’m not suggesting it’s an easy fix, but it is clearly and obviously fixable.

Besides, isn’t it a bit rash to blame an OS used by uncounted thousands when a mere 5 developers worldwide have complained about this issue in the entire time since 4.4 was released?!

For example, I note that the original reporter makes no mention of having a focus listener at all – just as an example, as that’s one of things you’d want to properly implement immersive – did they forget even that?  And if so what ELSE might they have missed?

Because there are hundreds upon hundreds of “A-list” apps that restore immersive just fine on the same exact devices where Corona apps consistently fail – that doesn’t suggest an unsolvable flaw in the OS.

A great and overwhelming preponderance of evidence is not in Corona’s favor if your engineers just throw up their hands in defeat and jump in with these other 5 who “blame the OS”.

Dave, I get it.  You want the immersive feature to work without issue.  We do too.

I’m one of the developers here and after a full day of testing and experimenting, yes, I’ve confirmed this to be a bug in the Android OS.  Google has confirmed it too.  That’s why it’s an open/assigned bug in Google’s issue tracker that Rob linked you to.

The problem is that the OS is drawing a black bar on top of the application at the bottom of the screen when it shouldn’t.  We have no control over that.  Nor have we found a way to detect that this is happening.  ***That’s the problem.***

The OS is in a bad state and drawing a black rectangle where the navigation bar used to be, but it also acts like it isn’t there either.

I’m sure that there must be a work-around for this issue.  Or perhaps it only works for very particular app activity configurations (the rumor is landscape-only apps).  Unfortunately, we don’t know what it is.  Nor do many other native Android developers.  But until Google/someone provides an official solution for this issue, we’re all stuck.

Sad to hear that :(,

I was hoping there would some kind of hack or workaround over this issue.

Guess i have to live with the on screen controls for now.

Have been holding my release for this fix.

Dave, have you experimented with this issue in native code? I haven’t yet myself, but I may take a shot at it.

I suppose it’s tempting, but no.  Despite my frustrated/accusatory “tone” at times, I don’t actually think I’m any “smarter” than the Corona engineers (fe Joshua) who’ve attempted to resolve the problem.  So I trust that they know what they’re doing, so my efforts would just be a duplication/waste of time.  In fact, even if I did manage to find a work-around, there’d be no guarantee that it could be “patched” into the rest of Corona’s “framework” as is, so Corona would still have to do all “same” work.

The best I could hope for is to “prove” that a workaround exists – but as I’ve already elaborated, the app store is already chock full of such “proof”.

For example, I find this an interesting case:  prior to v5, Unity itself did not support immersive, so their community took up the task.  I just downloaded a precompiled demo from (what appears to be) the most respected such plugin:

https://play.google.com/store/apps/details?id=com.ruudlenders.immersivemode

It works flawlessly re the no black bar on resume. (on my same ‘problem device’ that so faithfully reproduces it)  So that plugin author must have figured out a workaround that at least worked for Unity 4.

p.s. Ruud Lenders (author of that Unity 4 immersive plugin) just shared his source w me, which would be a jump-start for such experimenting:  comment out bit by bit til problem resurfaces, put back a bit to verify it resolves, then say “aha!”  :)  but to what end?  i’d need enterprise (and don’t want it) to actually benefit from that knowledge.

Dave… perhaps you could share the source (with Ruud’s permission) so that maybe Corona’s engineers (and even some of us engineers who don’t work for Corona) could identify a solution.

yep, was just awaiting his reply, now have his permission.

attachments seem to be disallowed, got: (You can upload up to  Uploading is not allowed  of files (Max. single file size:  230MB ))

i guess it’s small enough for a code block, his ActivityLIstener.java:

package com.ruudlenders.immersivemode; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.app.Application.ActivityLifecycleCallbacks; import android.os.Build; import android.os.Bundle; import android.view.ActionMode; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.view.ActionMode.Callback; import android.view.WindowManager.LayoutParams; import android.view.accessibility.AccessibilityEvent; @TargetApi(19) public class ActivityListener implements ActivityLifecycleCallbacks, Window.Callback, View.OnSystemUiVisibilityChangeListener { private static final String TAG = "ActivityListener"; private Activity activity; private Window window; private Window.Callback callback; private View decorView; private int visibility; public Activity getActivity() { return activity; } public ActivityListener(Activity activity) { Log.vBegin(TAG, "ImmersiveListener"); this.activity = activity; if (activity != null) { activity.getApplication().registerActivityLifecycleCallbacks(this); window = activity.getWindow(); if (window != null) { callback = window.getCallback(); window.setCallback(this); decorView = window.getDecorView(); if (decorView != null) { if (Build.VERSION.SDK\_INT \>= 16) { visibility = decorView.getSystemUiVisibility() & (View.SYSTEM\_UI\_FLAG\_LOW\_PROFILE | View.SYSTEM\_UI\_FLAG\_FULLSCREEN | View.SYSTEM\_UI\_FLAG\_LAYOUT\_STABLE | View.SYSTEM\_UI\_FLAG\_LAYOUT\_FULLSCREEN); } final ActivityListener listener = this; activity.runOnUiThread(new Runnable() { @Override public void run() { decorView .setOnSystemUiVisibilityChangeListener(listener); apply(); } }); } } } Log.vEnd(TAG, "ctor"); } public void close() { Log.vBegin(TAG, "close()"); if (activity != null) { activity.getApplication() .unregisterActivityLifecycleCallbacks(this); if (window != null) { window.setCallback(callback); if (decorView != null) { decorView.post(new Runnable() { @Override public void run() { decorView .setOnSystemUiVisibilityChangeListener(null); apply(); decorView.setSystemUiVisibility(visibility); } }); } } } Log.vEnd(TAG, "void"); } public void apply() { Log.vBegin(TAG, "apply()"); if (decorView != null) { int visibility = this.visibility; if (Build.VERSION.SDK\_INT \>= 14) { if (ImmersiveMode.isLowProfile()) { visibility |= View.SYSTEM\_UI\_FLAG\_LOW\_PROFILE; } if (Build.VERSION.SDK\_INT \>= 16) { visibility |= View.SYSTEM\_UI\_FLAG\_LAYOUT\_STABLE; if (Build.VERSION.SDK\_INT \>= 19) { visibility |= View.SYSTEM\_UI\_FLAG\_HIDE\_NAVIGATION | View.SYSTEM\_UI\_FLAG\_LAYOUT\_HIDE\_NAVIGATION | View.SYSTEM\_UI\_FLAG\_IMMERSIVE\_STICKY; } } } decorView.setSystemUiVisibility(visibility); } Log.vEnd(TAG, "void"); } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(Activity activity) { apply(); } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { } @Override public boolean dispatchGenericMotionEvent(MotionEvent event) { return callback != null && callback.dispatchGenericMotionEvent(event); } @Override public boolean dispatchKeyEvent(KeyEvent event) { return callback != null && callback.dispatchKeyEvent(event); } @Override public boolean dispatchKeyShortcutEvent(KeyEvent event) { return callback != null && callback.dispatchKeyShortcutEvent(event); } @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { return callback != null && callback.dispatchPopulateAccessibilityEvent(event); } @Override public boolean dispatchTouchEvent(MotionEvent event) { return callback != null && callback.dispatchTouchEvent(event); } @Override public boolean dispatchTrackballEvent(MotionEvent event) { return callback != null && callback.dispatchTrackballEvent(event); } @Override public void onActionModeFinished(ActionMode mode) { callback.onActionModeFinished(mode); } @Override public void onActionModeStarted(ActionMode mode) { callback.onActionModeStarted(mode); } @Override public void onAttachedToWindow() { callback.onAttachedToWindow(); } @Override public void onContentChanged() { callback.onContentChanged(); } @Override public boolean onCreatePanelMenu(int featureId, Menu menu) { return callback != null && callback.onCreatePanelMenu(featureId, menu); } @Override public View onCreatePanelView(int featureId) { return callback != null ? callback.onCreatePanelView(featureId) : null; } @Override @SuppressLint("MissingSuperCall") public void onDetachedFromWindow() { if (callback != null) { callback.onDetachedFromWindow(); } } @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { return callback != null && callback.onMenuItemSelected(featureId, item); } @Override public boolean onMenuOpened(int featureId, Menu menu) { return callback != null && callback.onMenuOpened(featureId, menu); } @Override public void onPanelClosed(int featureId, Menu menu) { callback.onPanelClosed(featureId, menu); } @Override public boolean onPreparePanel(int featureId, View view, Menu menu) { return callback != null && callback.onPreparePanel(featureId, view, menu); } @Override public boolean onSearchRequested() { return callback != null && callback.onSearchRequested(); } @Override public void onWindowAttributesChanged(LayoutParams attrs) { callback.onWindowAttributesChanged(attrs); } @Override public void onWindowFocusChanged(boolean hasFocus) { Log.vBegin(TAG, "onWindowFocusChanged(" + hasFocus + ")"); callback.onWindowFocusChanged(hasFocus); if (hasFocus && decorView != null) { decorView.postDelayed(new Runnable() { @Override public void run() { apply(); } }, 1000); } Log.vEnd(TAG, "void"); } @Override public ActionMode onWindowStartingActionMode(Callback callback) { return this.callback != null ? this.callback .onWindowStartingActionMode(callback) : null; } @Override public void onSystemUiVisibilityChange(int visibility) { Log.vBegin(TAG, "onSystemUiVisibilityChange(" + visibility + ")"); apply(); Log.vEnd(TAG, "void"); } }

i only have the java side of the actual plugin, not the .cs scripts on the unity side, but all those do is “configure it” (fe ImmersiveMode.isLowProfile), all the important stuff happens here

I haven’t studied it “deeply”, but on a casual read it doesn’t appear to be doing anything not covered here:

http://developer.android.com/training/system-ui/immersive.html

he’s using the expected flags, has the three listeners (resume,focus,uivis), and start/stop of course…

anyone reading this thread have an enterprise project they can tinker with?

a tidbit as fyi:  orientation doesn’t seem to matter - i can replicate the problem with Corona in either portrait or landscape.  and i can (or rather have, already, in this thread) point to other non-Corona immersive apps in either orientation that don’t experience the problem.  so just as a variable you can probably rule out.

I’m interested in learning if this proves useful to the Corona engineers - it seems like it would.

Just to be clear to everyone on this thread, engineering is not going to dig into this issue in the next couple of months.  Our *top* priority is Android 6.0 and dynamic permission support… which is a huge job in itself.

Thanks for posting the above code, but we’re already effectively doing exactly that.  We suspect that the OS has an issue with how the activity window is configured.  Unfortunately, issues like this are never easy.

my own purpose here is just a hope to get it relisted as a still-open issue, regardless of timeline to address, as opposed to an “os defect/won’t fix” closed issue

Fyi, from https://code.google.com/p/android/issues/detail?id=170752

It seems this has been (at least partially) fixed in Android 6.0.1 (Cant reproduce bug.)

I will test later today hopefully.

Now Dave, before you blow a gasket, I know this isn’t a fix for most existing users, but it’s worth looking at and considering.

i’ll be glad to have it resolved in 6, tho old os’s tend to stay around a lot longer on android than ios.

and no gasket blown, cuz that still wouldn’t indicate a true flaw in the os.  perhaps it’s just semantics, but if it’s possible (and it is) to get it working on 4 and 5, then where is the “flaw”?  just because it’s possible to implement something in a way that doesn’t fully function correctly doesn’t mean there’s an os flaw.

more likely it’s just a really really finicky feature with poor documentation of some “missing step” in the implementation, and perhaps that missing step is no longer required by 6 to function properly.  (“missing step” as generic, since we don’t really have any idea of the true cause, it could even be some bizarre interrelationship with some other build setting that you’d never logically guess at)

given those semantics, then maybe 6 just “simplifies the implementation” rather than “fixes a flaw” == no gasket blown, and i’d still like to have it fixed/patched/worked-around/“whatever” on 4/5 when engineering time allows.

Just another “clue” for engineers, if/when they ever revisit this problem…

This “bad state” occurs following a resume from task list – whether or not you’ve ever set immersive previously.  That is, the conditions for subsequent failure are produced during resume, though the failure itself is not manifested until next attempt to set immersive.  I don’t see any reason to suspect the OS itself is in a “bad state”, rather that the app is in a “bad state”.

It’s a subtle cause/effect distinction, and I’ve tried to make the point before but not sure I succeeded, perhaps some code will help.  (apologies again, but uploads are still disabled)  Here’s a simple/minimal app with no “automatic” immersive code, just some manual buttons, there will be two tests to run with it (below):

main.lua

display.setStatusBar(display.HiddenStatusBar) local background = display.newRect(display.contentCenterX, display.contentCenterY, 5000, 5000) background:setFillColor(0,0,1) local widget = require("widget") widget.setTheme("widget\_theme\_android\_holo\_light") local uivisDefaultButton = widget.newButton({ left = 100, top = 100, label = "default", onPress = function() native.setProperty("androidSystemUiVisibility","default") end }) local uivisImmersiveStickyButton = widget.newButton({ left = 100, top = 200, label = "immersiveSticky", onPress = function() native.setProperty("androidSystemUiVisibility","immersiveSticky") end })

config.lua

application = { content = { -- don't need any of this, use unscaled device resolution }, }

build.settings

settings = { orientation = { default = "portrait", supported = {"portrait"} } }

Prep:  find a device that reliably reproduces problem, build & install app.

The first “success” test:  launch the app fresh (not previously running), press buttons to set/unset immersive, all works as expected.  done, kill the task/force quit.

The second “failure” test:  launch the app fresh (not previously running), press the circle home button on nav bar to suspend, press the square task list button and select this app to resume.  App is right now in the “bad state” although it may not be obvious because it hasn’t yet been manifested by attempting immersive, _and even though we’ve not yet used immersive at all _, but any subsequent attempts to use immersive during this activation will fail with black bar - so now press the immersive button and witness.  Now power off, power on, resuming app.  The “bad state” is no longer present, and again immersive will work just fine for the duration of this activation.

So it’s this resume from task list itself that somehow appears to set things up for subsequent failure.  Using immersive at any other time, via any other method of launching or resuming, works just fine.  But some “condition” of the app must be different following a resume from task list.

The subtle distinction is:  immersive seems to be the victim of this “bad state” rather than the cause of it.

The cause seems to be some difference in the app following a resume from task list.  Resume from any other method (power cycle, icon) and the bad state is again cleared and immersive works properly.

The reason for trying to highlight this subtlety is,… maybe there’s nothing wrong at all with the way immersive itself is implemented.  Maybe it’d be a total waste of engineering time to reconsider that specific code.  Because maybe there’s something wrong “elsewhere”, say in the resume logic somehow, that perhaps leaves the window/view/layout/glsurface/other in this “bad state” - a state whose “bad-ness” lies dormant until manifested by subsequent immersive attempts.  So maybe that’s where engineering time should be spent?

Ok, after many edits I know when this is happening.

If you use:

local function onSystemEvent( event ) if event.type == "applicationResume" then native.setProperty( "androidSystemUiVisibility", "immersiveSticky" ) end end Runtime:addEventListener( "system", onSystemEvent )

It will work fine, except when you resume the application from the Task Menu while the phone is in landscape view (i’ve tested this only in landscape).

This is why when you resume your application from your app logo (from the phone menu or the “desktop”) the black bar isnt there, the phone is currently in portrait mode (original mode for the phone) and it changes to landscape for your application. In that case, the black bar is not showing.

This also works for the Task Menu when you are, for example, using chrome in portrait mode and you press the “square button” and then enter your application. The black bar is not showing there.

So, even if the code above works, in those scenarios where the user is playing another game in landscape mode, or when he is inside your application and presses the square button to check the task menu but goes back to your game, the black bar is there screwing up your application.

Note1: I’m using a Moto G (2013) with Android 5.1

Note2: We already know that sleep mode when playing your application and then resuming works fine, there is no black bar. Now, even if the black bar showed, if the application goes to sleep mode and you resume it, the bugged bar is done.

Conclusion: There is no oficial fix, but we can probably find a workaround. The idea is to reset the position somehow or trick the application with sleep mode or something like that (read note2, its important)

If you have any question please let me know, it’s late and I might not be explaining it as good as I can.

By the way, showing interstitial ads (at least adMob) triggers the soft keys, even if it has an “X” button, you can dismiss it either the back button or “X” button. If you use the function in main.lua as a global, when the ads finish, the applicationResume system event triggers and sends it back to immersiveSticky.

I was just revisiting this problem to see if anything had changed with recent builds (unfortunately does not appear so)

however, I do have some new info to offer… (might be worth a quick re-read of the exchange between myself and Joshua Quick on 5 May 2015 as prep)

I’m going back to my original assertion that resizing of the view does NOT occur (following suspend/resume, restoring immersive sticky, bar hiding, black area remaining, and despite the resize event)

the “black bar at bottom” is NOT some leftover OS “overlay” remnant – it is a “void” area caused by the view not resizing to fill the full-screen immersive screen dimensions.

yes, a resize event occurs, and yes, the full screen dimensions are reported, but NO – the app’s view is NOT actually resized. (I’m willing to believe the the GL surface is being resized to fullscreen, but it must be getting cropped/clipped by the app’s view which is NOT resized to fullscreen.)

how do I know?  android sdk monitor.  See linked image.  This is on a Nexus 7 2013 5.0.2.   You see that the view’s size upon resume/restore immersive is less than (1824) fullscreen (1920) and there are no UI elements at all in that black void at screen bottom.  (and you can’t see it, but at the bottom of the app’s blue background is a little “marker” rect that WOULD be visible if the entire surface were occupying the entire screen)

so, potentially: GL surface resizing, app view not resizing?  does that perhaps offer any new clues to reopen the investigation of this?  thanks for your consideration

ae4ksn.png