I am having an inconsistent user experience due to the way android navigates back from Android Settings.
In my application the user needs to give my app access to ACTION_USAGE_ACCESS_SETTINGS, which I access with the following:
Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
After toggling the setting to on for my application, I need the user to return to my application. The only way to do this that I know of is for them to press the back button on the phone ( would love to know if it is possible to return automatically after the setting has been toggled!!!?!).
Now one of two things will happen:
1) The user has not used android settings recently, so it was not already open ( ie open in the open app drawer). The first press of the back button will take them to my application as desired.
2) The user had used android settings recently. Thus settings was already open in the application drawer. Now when the user presses back, Android will take them back through each setting page they had been using recently (ie the back button takes them through their history in the android settings pages). It may take 2, 3 or 4 presses of the back button to leave Android settings, and return to my application. This is obviously terrible UI/UX, and I was wondering if there is a better way?
I have noticed that when installing Google apps, after toggling the setting to ON, it automatically exits and returns to the application that called the setting. Being able to do that would be ideal, but I just cant work it out.
Thanks!
Ok, so after trying about a millions things, I have come up with 3 different ways to improve my problem.
1) Was provided by #CommonsWare, by removing the FLAG_ACTIVITY_NEW_TASK, it prevented an inconsistent number of back presses needed, and means every time the user would only have to press back twice.
2) Upon further research into the flags, I found that using three combined:
Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
startActivity(intent);
reduced the number of back presses required to 1 single back press. This is simple and reasonable user friendly.
3) Using a handler to continuously check for the required permission after the settings intent has fired. This seems a like a bit of a hack, and I can't believe there is not a better way to do this, but it works exactly as it works when using a Google App. Ie, as soon as you switch the toggle to on, it exits the Android Settings page, and returns to your application, where you left off. I am using:
Handler handler = new Handler();
Runnable checkSettingOn = new Runnable() {
#Override
//#TargetApi(23)
public void run() {
Log.d(TAG, "run: 1");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.d(TAG, "run: 2");
return;
}
if (isAccessGranted()) {
Log.d(TAG, "run: 3");
//You have the permission, re-launch MainActivity
Intent i = new Intent(MainActivity.this, MainActivity.class);
Log.d(TAG, "run: 4");
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(i);
return;
}
handler.postDelayed(this, 200);
}
};
Then when you fire the intent to get to the Android settings page, just be sure to start the handler:
handler.postDelayed(checkSettingOn, 1000);
Hope this helps someone else with a similar issue.
After you have started Settings activity you can run periodical task, that is invoked every, for example 500ms, and checks if permission is granted.
Mark your setup activity with flag singleTask/singleInstance, and start it, if permission is granted. Existing instance of activity will be moved to top.
I did this for notifications access permission.
Extending upon Ufkoku's answer, I used a periodic check on the status of the permission. To get the user back, I used startActivityForResult and finishActivity.
Sample Code : I used this for the "USAGE_ACCESS_SETTINGS".
var intent = Intent("android.settings.USAGE_ACCESS_SETTINGS")
intent.data = Uri.fromParts("package", "com.example.app", null)
startActivityForResult(intent,101)
var timer = Timer(true)
timer.scheduleAtFixedRate(object: TimerTask(){
override fun run(){
if(checkUsageAccessPermission()){ // checkUsageAccessPermission() is a helper function
finishActivity(101)
cancel()
}
}
}, 0, 1000)
The solution from #Geordie Wicks was close to what I was looking for, but it seems like having the Android Settings open before launching your intent would cause the back button to take you to the previous Android Settings screen.
My solution involves two more flags, Intent.FLAG_ACTIVITY_CLEAR_TOP and Intent.FLAG_ACTIVITY_CLEAR_TASK.
You can define your intent as below:
Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
From my experience, this will make it so that a back button press will take you back to your application regardless of whether or not the Android Settings were open beforehand.
You can do one thing here. you have to confirm that you only open the settings component that you require to be opened .... Do not open the main settings app. Open only a single component within settings app that you want. So in that way when ever you will press back, you will jump back to your application.
For example if i want to open bluetooth settings in settings
application, I wont open the main Settings app instead i will open
only bluetooth component of settings. In that way when i press back i
will return to my own app because i haven't open the main setting app so i do not need to navigate in it.
ComponentName cn = new ComponentName("com.android.settings",
"com.android.settings.bluetooth.BluetoothSettings");
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", getPackageName(), null); intent.setData(uri); startActivity(intent);
This is also the simplest way to navigate it
Sorry if the answer was late but I left my answer here for others to overcome this problems. To me the problems might be ourself not from Android. For e.g I have a listener, which listen to the state of the switch
setOnCheckedChangeListener { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data =
Uri.fromParts(
"package",
navigator.activeFragment?.activity?.packageName,
null
)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
navigator.activeFragment?.startActivity(intent)
}
What happened with this code?
The user go to Setting and then navigate back. If you have set something that update the UI like belows here after user go back, then this will cause multiple listener listen to the change of switch
override fun onResume() {
super.onResume()
if (!isFirst) {
// Notify that user might have change the notification setting
(binding.rvSetting2.adapter as SettingAdapter).notifyItemChanged(0)
}
isFirst = false
My solution
You may add this line inside the listener, this will remove the current switch listener, which can avoid go to Setting after and after when switch change.
setOnCheckedChangeListener { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data =
Uri.fromParts(
"package",
navigator.activeFragment?.activity?.packageName,
null
)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
navigator.activeFragment?.startActivity(intent)
// Remove the current listener in case of user went back from Setting and call on bind view holder again
// This cause multiple listener listen to the change of this switch
setOnCheckedChangeListener(null)
}
I know that this is just my personal solution, but if you share similar ways, this might be help.
You have to confirm that you only open the settings of your app that you require to be opened. Do not open the main settings app. Open only your app settings that you want. So in that way whenever you will press back, you will jump back to your application.
Uri uri = Uri.fromParts("package", getPackageName(), null);
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(uri);
startActivity(intent);
As you know, android provided Multi-Window support mode in android N. Our application has multi-window support.
But how to test it? How to force test run the app in that mode? I haven't founded any such method in Instrumentation class or anywhere else in documentation. Maybe it is somehow possible with Espresso?
Unfortunately the way provided by azizbekian requires an app which has been loaded in multi-window mode previously, so I want to provide own solution. At the answer I have found how to enter multi-window mode programmatically. Using it I built the complete solution:
UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
//enter multi-window mode
uiAutomation.performGlobalAction(AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN);
//wait for completion, unfortunately waitForIdle doesn't applicable here
Thread.sleep(1000);
//simulate selection of our activity
MotionEvent motionDown = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), KeyEvent.ACTION_DOWN,
150, 200, 0);
motionDown.setSource(InputDevice.SOURCE_TOUCHSCREEN);
uiAutomation.injectInputEvent(motionDown, true);
motionDown.recycle();
MotionEvent motionUp = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), KeyEvent.ACTION_UP,
150, 200, 0);
motionUp.setSource(InputDevice.SOURCE_TOUCHSCREEN);
uiAutomation.injectInputEvent(motionUp, true);
motionUp.recycle();
//perform test actions below
As you can see, there are two workarounds:
We can't use uiAutomation.waitForIdle to wait entering multi-mode completion
I haven't found a way to select an application in task manager to request focus to our activity. So I just perform some touch event on the possible location of our activity.
After implementing it you'll be able to test the activity as usual with Espresso etc.
From Launch New Activities in Multi-Window Mode:
When you launch a new activity, you can hint to the system that the new activity should be displayed adjacent to the current one, if possible. To do this, use the intent flag FLAG_ACTIVITY_LAUNCH_ADJACENT.
From docs of Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT:
This flag is only used in split-screen multi-window mode. The new activity will be displayed adjacent to the one launching it. This can only be used in conjunction with FLAG_ACTIVITY_NEW_TASK. Also, setting FLAG_ACTIVITY_MULTIPLE_TASK is required if you want a new instance of an existing activity to be created.
As shown here how to start activity under test:
#Test
public void customIntentToStartActivity() {
Intent intent = new Intent(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT)
| Intent.FLAG_ACTIVITY_NEW_TASK);
mActivity = mActivityRule.launchActivity(intent);
}
Note, this is my guess based on documentation, haven't tried it. Although, it seems to me you have to start a "fake" Activity first, and from there launch tested activity in multi-window mode, because "The new activity will be displayed adjacent to the one launching it", so there should be another activity who launches it with specified Intent flags.
My problem is: I can't figure out a way to restart my activity from itself. For example in a game, if you die, there's a menu with an option to retry, which restart the stage instantly. That's exactly the case.
As i have read from a lot of others answers on this matter, there are 2 main methods for reloading an activity:
1- finish();
startActivity(getIntent());
2- recreate();
The first method doesn't seem to work for me because it restart the activity, but leave the previous created one open on the back of my stack. Looks like the finish() statement is not doing its stuff. I have tried to put it in front, after, and all kind of ways, but it never closes the activity left behind.
On the other hand, the option number 2 cannot be called (as far as i know) from outside the UI thread, and if i do, the app crashes. I tried to call the recreate method using:
Activity.this.runOnUiThread(new Runnable() {
public void run() {
recreate();
}
});
But this approach gives me a very awful blinking on my screen. A black screen suddenly appears, and that doesn't seems right.
Try this:
Intent intent = getActivity().getIntent();
getActivity().finish();
startActivity(intent);
You should add FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK flags to the intent, such as:
Intent intent = getActivity().getIntent();
intent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
getActivity().finish();
startActivity(intent);
This will force the system to remove the previous task from the stack.
I am trying to add bluetooth support to my OpenGL based Android game.
However, when setting a new intent startActivity or startActivityForResult the OpenGL render in the background goes black.
I am starting the intent from within a pop up window so you can see the OpenGL render in the background up until the point the new intent kicks in.
The intent I am setting is BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION
When an alert show for confirmation only when I press it it turns black, not before.
The code I am using to create the new intent is:
if (finalAdapter.getScanMode() !=
BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
discoverableIntent.putExtra("Theme", "Transparent");
a.startActivityForResult(discoverableIntent, 42);
}
I have tried to set the style to make the intent transparent and show the OpenGL thinking maybe the intent's background is Opaque.
Even if I accept the intent being black opaque I can't even restore the OpenGL rendering after attempting to remove this intent with finishActivity
The code for removing the intent is
cancelButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
a1.finishActivity(42);
self.dismiss();
}
});
The game logic still works and the render update function is still being called. It's just that the screen is black.
Maybe I need to reload the OpenGL resources? Which would seem odd?
Is the intent actually being removed? Why do I get a black screen?
Thank you.
Ok, Apparently when I call startActivity onPause and onResume are being called respectively for the main activity.
I handle OpenGL in case of onRestart and onStop but when only onPause and onResume are called I don't reload OpenGL properly.
That was the issue.
I'm trying to explicitly launch an intent to a new Activity, but I would like some code in the current Activity to finish executing first. I've done a bit of research, and have a couple of ideas to accomplish this but am thinking "there's gotta be an easier way to do this". Here is the relevant block of code:
cpuToast(dmg);
if (player_.getStatus() == false)
{
playerWon_ = false;
Intent intent = new Intent(Main.this, Death.class);
startActivity(intent);
}
dmg is an int. cpuToast simply makes a String to display the damage and then calls show(). getStatus() returns whether or not the player was killed. In the event that player was killed, I launch a new intent which will play an animation of the players death. Unfortunately the "Death" Activity is being launched before the Toast even becomes visible, and then it becomes visible during the Death Activity which I do not want.
Does anyone know a simple way to ensure that the Toast executes fully prior to launching the Death Activity? From what I've found it looks like I'll have to create a "Timer" object when really all I want is a simple while loop like "while(Toast.isVisable) {}" to tie up the execution for a couple of seconds.
Some example code for updating the UI in response to timed events can be found at http://developer.android.com/resources/articles/timed-ui-updates.html.
At the time you begin your toast, you can also post a delayed message to your current activity; the runnable of that delayed message can start the new intent.
Does it have to be a toast?
If you are up for the idea of using a custom dialog (removed title, buttons etc.) that is probably your best bet. Use a handler to dismiss the dialog after given time and start the new activity.