Detect splash screen exit on Unity Android/Eclipse project - android

I've created an Eclipse project based on a Vuforia/Unity project by following the instructions here. That's up and running.
I am adding a button to my main activity, extends from QCARPlayerActivity. That also works, however, the button sits on top of the Unity player as the Unity splash screen plays.
Is there any way to detect when the Unity splash screen exits so I don't have controls in place before the scene loads?
UPDATE 3/18/13
I've added a static boolean to my main activity in Eclipse to track splash screen completion and modified the code that adds the controls to watch the boolean.
MainActivity.java
public static boolean splashComplete = false;
private View mControlViewContainer = null; // initialized in onCreate
class QCARViewFinderTask extends TimerTask {
public void run() {
MainActivity.this.runOnUiThread(new Runnable() {
public void run() {
if (!QCAR.isInitialized()) return; //wait for QCAR init
//search for QCAR view if it hasn't been found
if (mQCARView == null)
{
View rootView = MainActivity.this.findViewById(android.R.id.content);
QCARUnityPlayer qcarView = findQCARView(rootView);
if (qcarView != null) {
mQCARParentView = (ViewGroup)(qcarView.getParent());
mQCARView = qcarView;
}
}
// add controls if QCAR view is located and the splash sequence is complete
if(mQCARView != null && splashComplete && mControlViewContainer != null){
mQCARParentView.addView(mControlViewContainer);
mViewFinderTimer.cancel();
mViewFinderTimer = null;
}
}
});
}
}
In Unity I created a simple script to set the static boolean in Java and attached it to the Vuforia ARCamera
SplashExit.js
function Start () {
var mainActivity = new AndroidJavaClass ("com.example.app.MainActivity");
mainActivity.SetStatic.("splashComplete",true);
}
This works fairly well in a project with a simple scene. My controls seem to load on splash exit. When I use this method with a more complicated scene, however, the controls come up a second or so before the splash screen disappears.
Is there a better place to attach my Unity script, or a better method within the script, that will more accurately reflect when the splash sequence has exited? Perhaps Jerdak's suggestion in the comments?

Adding a yield statement did the trick. Full solution follows.
SplashExit.js should be attached to the ARCamera Game object in Unity. The start method will stall until the scene has loaded up, then set splashComplete to true in MainActivity.java.
As the timer in MainActivity.java repeatedly calls the run method of QCARViewFinderTask, the control view will be added to the Unity Player parent view as splashComplete transitions to true.
MainActivity.java
public class MainActivity extends QCARPlayerActivity {
private QCARUnityPlayer mQCARView = null;
private ViewGroup mQCARParentView = null;
private Timer mViewFinderTimer = null;
private View mControlViewContainer = null;
public static boolean splashComplete = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mControlViewContainer = getLayoutInflater().inflate(R.layout.control_layout, null);
}
#Override
public void onResume() {
super.onResume();
if (mQCARView == null) {
//search the QCAR view
mViewFinderTimer = new Timer();
mViewFinderTimer.scheduleAtFixedRate(new QCARViewFinderTask(), 1000, 1000);
}
}
#Override
public void onPause() {
super.onPause();
if (mViewFinderTimer != null) {
mViewFinderTimer.cancel();
mViewFinderTimer = null;
}
}
class QCARViewFinderTask extends TimerTask {
public void run() {
MainActivity.this.runOnUiThread(new Runnable() {
public void run() {
if (!QCAR.isInitialized()) return; //wait for QCAR init
//search for QCAR view if it hasn't been found
if (mQCARView == null)
{
View rootView = MainActivity.this.findViewById(android.R.id.content);
QCARUnityPlayer qcarView = findQCARView(rootView);
if (qcarView != null) {
mQCARParentView = (ViewGroup)(qcarView.getParent());
mQCARView = qcarView;
}
}
// add controls if QCAR view is located and the splash sequence is complete
if(mQCARView != null && splashComplete && mControlViewContainer != null){
mQCARParentView.addView(mControlViewContainer);
mViewFinderTimer.cancel();
mViewFinderTimer = null;
}
}
});
}
private QCARUnityPlayer findQCARView(View view) {
if (view instanceof QCARUnityPlayer) {
return (QCARUnityPlayer)view;
}
if (view instanceof ViewGroup) {
ViewGroup vg = (ViewGroup)view;
for (int i = 0; i
SplashExit.js
function Start () {
yield; // wait for the scene to fully load
// Note that com.example.app.MainActivity should be updated to match your bundle identifier and class names
var mainActivity = new AndroidJavaClass ("com.example.app.MainActivity");
mainActivity.SetStatic.("splashComplete",true);
}

Related

Updating Views through AsyncTasks

Below is working sample code of an Android Activity that has 2 buttons, of which only 1 is ever visible: a refresh button and a stop button.
This code does the following:
Refresh is clicked: start some AsyncTasks. The preExecute of the tasks will hide the refresh button and show the stop button. The postExecute of the task will check if there are no more running tasks and if so, show the refresh button and hide the stop button.
Stop is clicked: all tasks are cancelled, the refresh button is shown and the stop button is hidden.
This code works fine, but for one exception: when I recreate the activity while a task is running by changing the screen orientation. The buttons will now return to the state as defined in the xml (refresh=visibile, stop=gone).
Using a static variable to keep track of the current state of the visibility only makes it worse, because the running Task that has to toggle it back, can only modify views in the calling activity, which has been stopped or destroyed at that point!
public class MainActivity extends Activity
{
private static List<MyAsyncTask> activeTasks = new LinkedList<MyAsyncTask>();
private View refresh;
private View stop;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
this.setContentView(R.layout.activity_main);
this.refresh = findViewById(R.id.refresh);
this.refresh.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{ // Start a couple tasks
new MyAsyncTask(MainActivity.this).execute();
new MyAsyncTask(MainActivity.this).execute();
}
});
this.stop = findViewById(R.id.stop);
this.stop.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{ // Cancel all tasks and toggle refresh button
cancelAll();
MainActivity.this.enableRefresh(true);
}
});
}
public void enableRefresh(boolean enable)
{
if (this.refresh != null && this.stop != null)
{
this.refresh.setVisibility(enable ? View.VISIBLE : View.GONE);
this.stop.setVisibility(!enable ? View.VISIBLE : View.GONE);
}
}
public static void cancelAll()
{
for (MyAsyncTask task : MainActivity.activeTasks)
task.cancel(true);
MainActivity.activeTasks = new LinkedList<MyAsyncTask>();
}
private class MyAsyncTask extends AsyncTask<Void,Void,Void>
{
private MainActivity activity;
public MyAsyncTask(MainActivity activity)
{
this.activity = activity;
}
#Override
protected void onPreExecute()
{
MainActivity.activeTasks.add(this);
this.activity.enableRefresh(false);
}
#Override
protected Void doInBackground(Void... v)
{
try
{ // Simulate a task
Thread.sleep(3000);
}
catch (InterruptedException e)
{
}
return null;
}
#Override
protected void onPostExecute(Void v)
{
MainActivity.activeTasks.remove(this);
if (MainActivity.activeTasks.size() == 0)
this.activity.enableRefresh(true);
}
}
}
http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
See this it will help you ... on handling the states in orientation change
to handle AsyncTasks on screen orientation follow this example
MyAsyncTask myasynce;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
this.setContentView(R.layout.activity_main);
this.refresh = findViewById(R.id.refresh);
this.refresh.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{
//Register new Task
myasynce = ( MyAsyncTask ) new MyAsyncTask(MainActivity.this).execute();
}
});
this.stop = findViewById(R.id.stop);
this.stop.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{ // Cancel all tasks and toggle refresh button
cancelAll();
MainActivity.this.enableRefresh(true);
}
});
}
now onSaveInstanceState add
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//put myasynce status true if still runing false if finished
outState.putBoolean("myasynce", ( myasynce != null && query.getStatus() != AsyncTask.Status.FINISHED ) ? true : false );
if ( myasynce != null )
{
myasynce.cancel(true);
}
}
on savedInstanceState
add
if ( savedInstanceState.getBoolean("myasynce") == true )
{
//if task was running before screen orientation run it again
myasynce = ( MyAsyncTask ) new MyAsyncTask(MainActivity.this).execute();
}
hope this help
I think this link http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html by Tony Stark is probably the best solution, because that potentially solves even more problems.
However, I think I came up with an simpler solution for the problem at hand:
Add static vars to MainActivity:
private static MainActivity current;
private static boolean enableRefresh = true;
Save input value of enableRefresh():
public static void enableRefresh(boolean enableRefresh)
{
MainActivity.enableRefresh = enableRefresh;
(...) // Same as before
}
Add to MainActivity onCreate():
MainActivity.current = this;
enableRefresh(enableRefresh);
In the AsyncTask, use
MainActivity.current as the activity to update, instead of the activity provided in the constructor.

ListFragment: prevent race condition between getListView() and onDestroyView()

I have a ListFragment that I want to update regulary. The update process itselfs is quite complicated and could take some time. That's why I made a thread that executes a new update 5 seconds after the previous update has been done. Then I create a handler to update the list, while keeping track of the position in the list.
The problem is that by quickly sliding between fragments in my ViewPager I can force a race condition: onDestroyView() can be called before the handler calls getListView(), resulting in the following error:
java.lang.IllegalStateException: Content view not yet created
My question is: how can I prevent this race condition? Is there any way to check if the view is still there? Checking if the updateThread has been interrupted in the code below is unfortunately not enough.
public class MyFragment extends ListFragment {
private Thread updateThread = null;
public void startUpdate() {
/* Kill old thread */
if (updateThread != null) {
updateThread.interrupt();
}
final Handler handler = new Handler();
Runnable r = new Runnable() {
#Override
public void run() {
while (true) {
// ... collect data in `adapter`
final ArrayAdapter<String[]> ada = adapter;
handler.post(new Runnable() {
#Override
public void run() {
if (ada != null) {
// restore view position
int index = getListView().getFirstVisiblePosition(); // CRASH here
View v = getListView().getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
setListAdapter(ada);
ada.notifyDataSetChanged();
getListView().setSelectionFromTop(index, top);
}
}
});
//... sleep for some second
}
}
};
updateThread = new Thread(r);
updateThread.start(); // start updating
}
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
startUpdate();
} else {
if (updateThread != null) {
updateThread.interrupt();
}
}
}
}
Try to use isAdded to check if you fragment is attached to activity

Android Loader vs AsyncTask on button tap

I have an activity which requires no data from server on load - just plain init for ui
UI has several buttons.
User clicks one of them and app sends request to server (rest call)
While request is processing spinner is shown (for about 10 seconds)
For now it uses AsyncTask - so if app changes portrait to landscape - activity is restarted and I loose the process
Second option is to use Loader - the problem is that it is started on button tap - not on activity start
This leads to many exceptions - when LoaderManager sends events to non-started item
Is there any solution?
few comments:
- 10 seconds is just for example
- lock user to one orientation is not an option
- service is overkill for simple rest call
public class TestActivity extends FragmentActivity {
private Button one;
private Button two;
private final int ONE_ID = 0;
private final int TWO_ID = 1;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
one = (Button) findViewById(R.id.one);
two = (Button) findViewById(R.id.two);
one.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
getLoaderManager().restartLoader(ONE_ID, null, callbacks);
}
});
two.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
getLoaderManager().restartLoader(ONE_ID, null, callbacks);
}
});
Loader<AsyncTaskLoaderResult<Result>> loader = getLoaderManager().getLoader(ONE_ID);
if (loader != null) {
getLoaderManager().initLoader(ONE_ID, null, callbacks);
}
loader = getLoaderManager().getLoader(TWO_ID);
if (loader != null) {
getLoaderManager().initLoader(TWO_ID, null, callbacks);
}
}
public static class AsyncTaskLoaderResult<E> {
public E data;
public Bundle args;
}
public static class Result {
}
private LoaderManager.LoaderCallbacks<AsyncTaskLoaderResult<Result>> callbacks = new LoaderManager.LoaderCallbacks<AsyncTaskLoaderResult<Result>>() {
#Override
public Loader<AsyncTaskLoaderResult<Result>> onCreateLoader(int id, Bundle args) {
/**
* according different Id, create different AsyncTaskLoader
*/
switch (id) {
case ONE_ID:
return new OneAsyncTaskLoader(TestActivity.this);
case TWO_ID:
return new TwoAsyncTaskLoader(TestActivity.this);
}
return null;
}
#Override
public void onLoadFinished(Loader<AsyncTaskLoaderResult<Result>> loader, AsyncTaskLoaderResult<Result> data) {
/**
* handle result
*/
switch (loader.getId()) {
}
getLoaderManager().destroyLoader(loader.getId());
}
#Override
public void onLoaderReset(Loader<AsyncTaskLoaderResult<Result>> loader) {
}
};
public static class OneAsyncTaskLoader extends AsyncTaskLoader<AsyncTaskLoaderResult<Result>> {
private AsyncTaskLoaderResult<Result> result;
public OneAsyncTaskLoader(Context context) {
super(context);
}
#Override
protected void onStartLoading() {
super.onStartLoading();
if (result != null) {
deliverResult(result);
} else {
forceLoad();
}
}
#Override
public AsyncTaskLoaderResult<Result> loadInBackground() {
/**
* send request to server
*/
result = new AsyncTaskLoaderResult<Result>();
result.data = null; // result.data comes from server's response
return result;
}
}
public static class TwoAsyncTaskLoader extends AsyncTaskLoader<AsyncTaskLoaderResult<Result>> {
private AsyncTaskLoaderResult<Result> result;
public TwoAsyncTaskLoader(Context context) {
super(context);
}
#Override
protected void onStartLoading() {
super.onStartLoading();
if (result != null) {
deliverResult(result);
} else {
forceLoad();
}
}
#Override
public AsyncTaskLoaderResult<Result> loadInBackground() {
/**
* send request to server
*/
result = new AsyncTaskLoaderResult<Result>();
result.data = null; // result.data comes from server's response
return result;
}
}
}
First, you can eliminate the orienatation change issue by declaring
android:configChanges="orientation"
or savedInstanceState()
But the real problem here is having the user stare at a spinner for 10 seconds. Most users aren't going to be patient enough for this. I don't know what your app is doing so its hard to give an accurate suggestion but I can say that you need to do your network stuff in your AsyncTask but allow the user to do other things
You can allow the user to do other things while the AsyncTask finishes or put that code in a [Service(http://developer.android.com/guide/components/services.html). Either way, don't make your users stare at a screen for 10 seconds of spinning...they won't be YOUR users for long
If you're using an AsyncTask for this you might want to either use a Service instead or use onRetainNonConfigurationInstance or Fragment.setRetainInstance to allow the AsyncTask to live through configuration changes.
Or disable configuration changes: I've used that in the past with some success.
Here's a good article on the subject:
http://www.javacodegeeks.com/2013/01/android-loaders-versus-asynctask.html
Anyways, as #codeMagic mentioned, AsyncTask with android:configChanges="orientation|screenSize" should be enough for you (it prevents activity from being recreated on config changes)

android on view removed from parent

on a child layout (View) is there a callback for when the view is removed from it's parent? I need to recycle some images when the view is done. I've been looking around on the web for what to do, but haven't found anything helpful yet.
I've been looking for something like this too. The best I can find is View.OnAttachStateChangeListener. I doubt it's ideal, as it's the callback for when the View is added & removed from the Window - not the parent, but it's sufficient for my needs.
Instead of registering a new listener, you can override onDetachedFromWindow in your custom View code.
I fall in that trap what marmor said:)
#Override
protected void onDetachedFromWindow() { I want to do something here, sometimes called sometimes not!!}
protected void onAttachedToWindow() {It is working fine, always}
This code is in a CustomView.
The calling code is:
contentHolder.removeAllViews();
// ... init my CustomView ...
contentHolder.addView(myCustomView);
contentHolder.requestLayout();// useless, not need
contentHolder.invalidate();// useless, not need
To understand why is not working you have to go inside Android API:
public void removeAllViews() {
removeAllViewsInLayout();
requestLayout();
invalidate(true);
}
public void removeAllViewsInLayout() {
final int count = mChildrenCount;
if (count <= 0) {
return;
}
final View[] children = mChildren;
mChildrenCount = 0;
final View focused = mFocused;
final boolean detach = mAttachInfo != null;
boolean clearChildFocus = false;
needGlobalAttributesUpdate(false);
for (int i = count - 1; i >= 0; i--) {
final View view = children[i];
if (mTransition != null) {
mTransition.removeChild(this, view);
}
if (view == focused) {
view.unFocus(null);
clearChildFocus = true;
}
view.clearAccessibilityFocus();
cancelTouchTarget(view);
cancelHoverTarget(view);
if (view.getAnimation() != null ||
(mTransitioningViews != null && mTransitioningViews.contains(view))) {
addDisappearingView(view);
} else if (detach) {
view.dispatchDetachedFromWindow();
}
if (view.hasTransientState()) {
childHasTransientStateChanged(view, false);
}
dispatchViewRemoved(view);
view.mParent = null;
children[i] = null;
}
if (clearChildFocus) {
clearChildFocus(focused);
if (!rootViewRequestFocus()) {
notifyGlobalFocusCleared(focused);
}
}
}
The key is here:
if (view.getAnimation() != null ||
(mTransitioningViews != null && mTransitioningViews.contains(view))) {
So, if you have animation ( and in 1 case I have and in 9 cases not) it will not called the onDetachedFromWindow() and it will mess the whole UI :)
public void endViewTransition(View view) {
if (mTransitioningViews != null) {
mTransitioningViews.remove(view);
final ArrayList<View> disappearingChildren = mDisappearingChildren;
if (disappearingChildren != null && disappearingChildren.contains(view)) {
disappearingChildren.remove(view);
if (mVisibilityChangingChildren != null &&
mVisibilityChangingChildren.contains(view)) {
mVisibilityChangingChildren.remove(view);
} else {
if (view.mAttachInfo != null) {
view.dispatchDetachedFromWindow();
}
if (view.mParent != null) {
view.mParent = null;
}
}
invalidate();
}
}
}
Again in some cases will be called even with animation.
addDisappearingView(view);
The accepted answer suggest something like this:
addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
#Override
public void onViewAttachedToWindow(View v) {
}
#Override
public void onViewDetachedFromWindow(View v) {
System.out.println("MyCustomView.onViewDetachedFromWindow");
}
});
Sadly on animation will not print the desired text.
Some important code from android.view.ViewGroup API:
void dispatchViewRemoved(View child) {
onViewRemoved(child);
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(this, child);
}
}
public void onViewRemoved(View child) {
}
So, you can override your RelativeLayout for this method.
My animation is an infinite animation, and it will not be called very soon any of the methods!
If you have an infinite animation the correct way is to write this code, when you call remove all views:
if(contentHolder.getChildCount() > 0 ){
View child0 = contentHolder.getChildAt(0);
Animation animation = child0.getAnimation();
if(animation != null) {
animation.cancel();
child0.clearAnimation();
}
}
contentHolder.removeAllViews();
Now it will be called the protected void onDetachedFromWindow()!
The Android KTX (Core KTX) library gives you a nice solution for this.
You'll need this dependency: androidx.core:core-ktx:1.3.0
You can then call a function "doOnDetach" to signal you want to run some code (once) when the view is removed from the window:
fun myInitCode() {
...
myView.doOnDetach(this::doOnMyViewDetachFromWindow)
...
}
fun doOnMyViewDetachFromWindow(view: View) {
... put your image cleanup code here ...
}
You can pass a lambda to "doOnDetach" but a method reference as shown above may be cleaner, depending on how much work you have to do.
The description of doOnDetach is as follows:
androidx.core.view ViewKt.class public inline fun View.doOnDetach(
crossinline action: (View) → Unit ): Unit
Performs the given action when this view is detached from a window. If
the view is not attached to a window the action will be performed
immediately, otherwise the action will be performed after the view is
detached from its current window. The action will only be invoked
once, and any listeners will then be removed.

Immediate App Crash, whenever single line of code is added

Here is a class that works perfectly until I wanted to add one more feature, The code compiles then crashes immediately upon execution. The problem lies with the componentAnalyzer class that I wish to implement in this class. I don´t know why it won't work because I implemented this componentAnalyzer in another class in the exact same way and it works beautifully.
I think its a small mistake but unsure. I commented out the part that was creating problems because the rest works and should not be touched.
The method that will use the componentAnalyzer is at the end of the code. I cut out everything that was working in order to see it easier.
public class PowerMonitorActivity extends Activity implements SeekBar.OnSeekBarChangeListener {
private static boolean instantiated = false;
//private static ComponentAnalyzer componentAnalyzer;
//private Context context;
private Button changeGPSButton;
private Button changeAudioButton;
private Button locationUpdateButton;
private SeekBar brightnessSeekBar;
private int screenBrightness = 123;
private Handler cpuHandler = new Handler();
private CPUMonitor cpuMonitor = new CPUMonitor();
private int updateTime = 1000;
private Handler totalPowerHandler = new Handler ();
private MediaManager mediaManager = new MediaManager();
private boolean requestingLocation = false;
private boolean wifiIsTransmitting = false;
private boolean wifiIsConnected = false;
private boolean cellIsTransmitting = false;
private boolean cellIsConnected = false;
private boolean isLogging = false;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//Bundle extras = getIntent().getExtras();
if (!instantiated) {
instantiated = true;
//componentAnalyzer = new ComponentAnalyzer(context, extras);
}
// Create GPS button - note that location settings cannot be changed directly
// in a program. Rather, a settings screen is shown when this button is pressed.
changeGPSButton = (Button)findViewById(R.id.changeGPS);
changeGPSButton.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
showGPSOptions();
}
});
totalPowerHandler.removeCallbacks(updatePower);
totalPowerHandler.postDelayed(updatePower, 1000);
}
private Runnable updateCpu = new Runnable() {
public void run() {
float util = cpuMonitor.getUtil();
int rutil = Math.round(100*util);
TextView cpuTextView = (TextView)findViewById(R.id.cpuTextView);
cpuTextView.setText(rutil+"%");
cpuHandler.postDelayed(this, updateTime);
}
};
private Runnable updatePower = new Runnable()
{
public void run()
{
//float testPower = componentAnalyzer.getWifiPower();;
TextView totalPowerView = (TextView)findViewById(R.id.totalPowerTextView);
//totalPowerView.setText(testPower+"mW");
totalPowerHandler.postDelayed(this, 1000);
}
};
Did you initialize context? In the current code example context is null when the ComponentAnalyzer is instantiated.
For my point of view, your application context is null (you forgot to assign reference to context for current activity or application)
Remove comments for problematic lines,
Now look at these lines,
Bundle extras = getIntent().getExtras();
if (!instantiated) {
instantiated = true;
componentAnalyzer = new ComponentAnalyzer(context, extras); // Here context is null
}
change this according to given below,
Bundle extras = getIntent().getExtras();
if (!instantiated) {
instantiated = true;
if(extras != null)
componentAnalyzer = new ComponentAnalyzer(PowerMonitorActivity.this, extras);
else Log.e("PowerMonitorActivity","Bundle extras is null");
}

Categories

Resources