I am trying to call an activity (activity A) from a service and what I want to happen is to check if there's already an instance of A on the stack, and if there is, to bring that to the top of the stack (and trigger onNewIntent() method), instead of always creating a new instance of A.
Wondering if this is possible. My activity uses the "singleTop" launchmode in the androidmanifest. The usual Intent.FLAG_ACTIVITY_NEW_TASK flag that is required to call an activity from outside an activity doesn't bring the already open activity A to the top of the stack, but always creates a new instance of A. Also it seems that when I use both flags (intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) the same thing happens (still 2 instances of the same activity). Is there a way to go about doing this, always keeping in mind that I am making the call from a service, and I don't want to pass the activity context to the service?
(P.S. I am using androidannotations, the call to open activity A occurs inside an #EBean, which itself is used in a Service. Is there an easy way to somehow pass that activity context to that #EBean?)
Since you want to use a non-activity context (e.g. applicationContext) to start the activity leading to use Intent.FLAG_ACTIVITY_NEW_TASK, you have only two choices to avoid creating a new instance of the target activity every time you're calling startActivity.
First, by specifying android:launchMode="singleInstance" in the manifest for the activity, you can force that only one instance of the activity should exist in a task that hosts only this instance. In this case, starting the activity brings it to the front if there exists in the host task, otherwise, a new task gets created containing the only instance of the activity. I think it's not the way we are looking for.
Second, by specifying android:launchMode="singleTask" in the manifest for the activity, we can achieve a better solution. In this case, the system creates a new task and adds the activity at the root of this task, if there exists no instance of the activity. Otherwise, the system brings the task containing the activity instance to the front then routes to the target activity, and calls onNewIntent.
Here is an example code of the second approach, examines 2 scenarios:
manifest.xml:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ActivityA" android:launchMode="singleTask" />
<activity android:name=".ActivityB" />
MainActivity.kt:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// First Scenario: ActivityA doesn't exist in back-stack
button1.setOnClickListener {
Intent(applicationContext, ActivityA::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}.let {
applicationContext.startActivity(it)
}
}
// Second Scenario: ActivityA exists in back-stack
button2.setOnClickListener {
startActivity(Intent(this, ActivityA::class.java))
// Start ActivityB after a while
Handler().postDelayed({
startActivity(Intent(this, ActivityB::class.java))
}, 1000)
}
}
}
ActivityA.kt
class ActivityA : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_a)
Toast.makeText(this, "onCreate on ActivityA", Toast.LENGTH_SHORT).show()
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
Toast.makeText(this, "onNewIntent on ActivityA", Toast.LENGTH_SHORT).show()
}
}
ActivityB.kt
class ActivityB : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_b)
button.setOnClickListener {
Intent(applicationContext, ActivityA::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}.let {
applicationContext.startActivity(it)
}
}
}
}
Result:
.......
Related
I am calling activity B from activity A using the ActivityResultLauncher and setting the result from activity B when the task is done. This works perfectly if orientation is not changed. The problem is when I change orientation from activity B and then setting the result, then registerForActivityResult of activity A is not called. Could someone let me know, what could be the issue?
Note: I do not face this issue if I am using startActivityForResult and onActivityResult. I changed this to ActivityResultLauncher as startActivityForResult became deprecated.
activity A sample code:
private lateinit var launcher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(layout)
setLauncherResult()
}
private fun setLauncherResult() {
launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
//Do operations here
}
}
//On button click starting activity B using launcher
val intent = Intent(activityA, activityB)
launcher.launch(intent)
}
activity B sample code:
//setting result
val intent = Intent()
//set intent extras
setResult(Activity.RESULT_OK, intent)
finish()
You can save the value in onSaveInstanceState() and use setResult in onRestoreInstanceState().
Try moving the registerForActivityResult assignment of launcher to onCreateView() (override it if you haven't already). Putting it here, I'm not getting the "is attempting to register while current state is STARTED" RuntimeException in OnCreateView() as it would with onResume() or later. The advantage of OnCreateView() over OnCreate() seems to be that the launcher variable will get re-set on theme day/night changes as well as just orientation changes.
Also see the Configuration change & Process death section here for more discussion.
I know the official dox say it's safe to set it before anything else in the class, but theme changes broke .launch(), and this location appears to work for me.
Everytime orientation changes it calls onCreate and it resets your views & Other variables like launcher are re-assigned. So you can follow the example shown in official document here: https://developer.android.com/training/basics/intents/result#launch
And pull your launcher assignment out of onCreate as following (This worked for me):
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
//do your stuff here
binding.tv.text = result.data?.getStringExtra("text")
}
}
companion object {
var number : Int = 0
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tv.setOnClickListener {
val intent = Intent(this, ActivityB::class.java)
startForResult.launch(intent)
}
}
}
If you don't want that onCreate should be called again when orientation changes. Then in your Manifest you can add configChanges:
<activity android:name=".MainActivity"
android:configChanges="orientation|screenSize">
I have HomeScreen and LoginScr activity and I would like to move from HomeScreen to LoginScr activity. I do it in such way:
val thread = Thread(Runnable {
try {
val client = APICallRequests.client
client.dispatcher.executorService.shutdown()
client.connectionPool.evictAll()
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
})
thread.start()
val intent = Intent(this, LoginScr::class.java)
startActivity(intent)
finishAfterTransition()
As you can see I try to close okHttp3 client, because maybe it keeps HomeScr activity alive. Also I use finishAfterTransition(). Then when I move from LoginScr to HomeScr I do it in such way:
private fun moveToNextScreen(){
val goToMain = Intent(this, HomeScreen::class.java)
startActivity(goToMain
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
)
finishAfterTransition()
overridePendingTransition(0, 0)
}
As I see I create two activity instances instead one. How I did it - added log to onCreate() fun. I totally don't understand why does it happen. Similar problem can be met after onBackPressed():
override fun onBackPressed() {
val startMain = Intent(Intent.ACTION_MAIN)
startMain.addCategory(Intent.CATEGORY_HOME)
startMain.flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
startActivity(startMain)
finish()
super.onBackPressed()
}
I tried a lot of variants but my HomeScreen can stay alone. I also tried such ways at manifest:
android:launchMode="singleTask"
android:noHistory="true"
Maybe someone know how to solve this problem?
I think this answer can help you
https://stackoverflow.com/a/10862977/13221053
Further, if you want to keep your app active in background then you can use services for that purpose, here is the documentation
https://developer.android.com/guide/components/services
I created another class where I have function of logout:
fun logOut(context: Context) {
context.stopService(Intent(context, CheckNewMessages::class.java))
val intent = Intent(context, LoginScr::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP and
Intent.FLAG_ACTIVITY_NEW_TASK and
Intent.FLAG_ACTIVITY_NO_ANIMATION
context.startActivity(intent)
(context as Activity).finish()
}
and as you can see I use this line for finishing activity:
(context as Activity).finish()
But it is still alive and as a result I have two or more same activities at my system. I tried a lot of ways like creating static variable at first activity and using this variable at the second one for closing. But my activity stays alive. I also tried to use lauchmode at manifest and some other ways. Maybe someone knows where I did a mistake?
UPDATE
Two places from which I call logOut(). 1st is interface between RV adapter and fragment:
override fun finish() {
APICallRequests.logOut(context!!)
activity!!.finishAffinity()
}
and 2nd at Interceptor for requests:
private fun updateAccessToken(context: Context) {
val sp = context.getSharedPreferences(Constants.SHARED_PREFS_STORAGE, 0)
synchronized(this) {
val tokensCall = accessTokenApi()
.getNewToken(ReqAccessToken(sp.getString("refresh_token", "")!!))
.execute()
if (tokensCall.isSuccessful) {
} else {
when (tokensCall.code()) {
500 -> {
val thread = object : Thread() {
override fun run() {
Looper.prepare()
Toast.makeText(cont, cont.getString(R.string.server_error_500), Toast.LENGTH_SHORT).show()
Looper.loop()
}
}
thread.start()
}
401 -> {
APICallRequests.logOut(context)
}
}
}
}
}
That's not the way it works. What happens is this. When you do:
context.startActivity(intent)
This doesn't start the new Activity immediately. It just requests that Android starts the new Activity when it gets control back. Then you do this:
(context as Activity).finish()
This call just finishes the current Activity. When you eventually return control to the Android framework, it will launch the new Activity as you requested in your call to startActivity().
If you want your app to exit (ie: all activities finished), you can just do:
(context as Activity).finishAffinity()
This call will finish the current Activity and all other activities in the task that belong to the same app.
NOTE: This only works if all activities in your app share the same affinity, which is the default case.
try to pass Activity instead of Context in inner param fun logOut(activity: Activity), this should help you if you are calling this function from activity. If you calling it from fragment you can use requareActivity.finish()
So i have a RegisterActivity and a MapsActivity.
When a new user opens the app, they will be on the RegisterActivity and sign up. Once they sign up, my app creates an Intent and sends them to the MapsActivity
The problem is whenever the user closes the app, they have to login/register again.
I want the the app to be able to open back to the MapsActivity since the user has not signed out yet
Here's my attempt:
i put an <intent-filter> in both RegisterActivity and MapsActivity because i think both should both be the main launchers depending if a user is logged in or not right?
If they aren't logged in, then the main launcher should be RegisterActivity
If they are logged in, then the main launcher should be MapsActivity
AndroidManifest.xml
<application
<activity android:name=".MapsActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".RegisterActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".LoginActivity">
</activity>
</application>
MapsActivity.kt
public override fun onStart() {
super.onStart()
// Check if user is signed in (non-null) and update UI accordingly.
val currentUser = FirebaseAuth.getInstance().currentUser
if(currentUser == null){
Log.d(TAG, "User is not logged in")
val intent = Intent(this, LoginActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK.or(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}else {
Log.d(TAG, "User is logged in")
}
}
The simplest way to achieve this is to use a listener. Let's assume you have two activities, the LoginActivity and the MainActivity. The listener that can be created in the LoginActivity should look like this:
val authStateListener = FirebaseAuth.AuthStateListener { firebaseAuth ->
val firebaseUser = firebaseAuth.currentUser
if (firebaseUser != null) {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
}
This basically means that if the user is logged in, skip the LoginActivity and go to the MainActivity.
Instantiate the FirebaseAuth object:
val firebaseAuth = FirebaseAuth.getInstance()
And start listening for changes in your onStart() method like this:
override fun onStart() {
super.onStart()
firebaseAuth!!.addAuthStateListener(this.authStateListener!!)
}
In the MainActivity, you should do the same thing:
val authStateListener = FirebaseAuth.AuthStateListener { firebaseAuth ->
val firebaseUser = firebaseAuth.currentUser
if (firebaseUser == null) {
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
}
}
Which basically means that if the user is not logged in, skip the MainActivity and go to the LoginActivity. In this activity you should do the same thing as in the LoginActivity, you should start listening for changes in the onStart().
In both activities, don't forget to remove the listener in the moment in which is not needed anymore. So add the following line of code in your onStop() method:
override fun onStop() {
super.onStop()
firebaseAuth!!.removeAuthStateListener(this.authStateListener!!)
}
I have developer a similar app and I simply use a splash screen that checks if the users is logged or not and then open the right screen.
The splash doesn't need to be to complex: in fact you can even avoid to set a view and just check whatever you want.
This article explains it very clearly.
Remember to call finish() on the splash activity after launching the right one.
I have a particular case where I need to ask something to the user when he starts a printer service.
So from the onStartPrinterDiscovery (so a service), I start an activity to display the dialog and when the action is done, I send a new intent which calls finish() nd so I see that onDestoy() is called.
Unfortunately when I hit the apps history button, I still see my activity's screen behind:
Could you tell me why and how to fix it please?
androidManifest.xml:
<activity
android:name=".DialogActivity"
android:noHistory="true"
android:theme="#android:style/Theme.Dialog"
android:launchMode="singleTop">
</activity>
DialogActivity:
class DialogActivity : Activity() {
var activity:Activity = this
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.floatingactivity)
setFinishOnTouchOutside(false)
onNewIntent(intent)
}
override fun onDestroy() {
super.onDestroy()
isDisplayed = false
}
override fun onNewIntent(intent: Intent) {
val extras = intent.extras
val action = extras.getString("action")
when (action) {
"showDialog" -> {
if (!isDisplayed) {
tvMessage.text = getString(R.string.ask_for_action)
isDisplayed = true
}
}
"showErrorDialog" -> {
if (!isDisplayed) {
tvMessage.text = getString(R.string.error_action)
isDisplayed = true
}
}
"dismissDialog" -> { activity.finish() }
else -> {}
}
if (isDisplayed) {
btCancel.setOnClickListener {
activity.finish()
}
}
}
companion object {
var isDisplayed = false
}
}
EDIT I add how I currently create my Intent because of one answer which could be a solution:
val intent = Intent(applicationContext, DialogActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
intent.putExtra("action","showDialog")
startActivity(intent)
Add the android:excludeFromRecents to your activity attributes, like so:
<activity
android:name=".DialogActivity"
android:theme="#android:style/Theme.Dialog"
android:excludeFromRecents="true"
android:launchMode="singleTop">
</activity>
This is from the docs of the android:excludeFromRecents:
Whether or not the task initiated by this activity should be excluded from the list of recently used applications, the overview screen. That is, when this activity is the root activity of a new task, this attribute determines whether the task should not appear in the list of recent apps. Set "true" if the task should be excluded from the list; set "false" if it should be included. The default value is "false".
The android:noHistory="true" is for different purposes. This is from the docs of the android:noHistory:
A value of "true" means that the activity will not leave a historical trace. It will not remain in the activity stack for the task, so the user will not be able to return to it. In this case, onActivityResult() is never called if you start another activity for a result from this activity.
Try this:-
Intent i = new Intent(this,YourFirstActivity.Class);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(i);
finish();
Or
use finishAffinity() suitable for >= API 16.