无论应用程序状态如何,都可以从通知中正确启动 Activity
Posted
技术标签:
【中文标题】无论应用程序状态如何,都可以从通知中正确启动 Activity【英文标题】:Properly start Activity from Notification regardless of app state 【发布时间】:2016-06-03 00:46:47 【问题描述】:我有一个带有启动屏幕 Activity 的应用程序,然后是一个主 Activity。启动主 Activity 之前,启动屏幕会加载内容(数据库等)。从这个主活动,用户可以导航到多个其他子活动并返回。一些子活动使用startActivityForResult()
启动,其他仅使用startActivity()
。
活动层次结构如下所示。
| Child A (startActivityForResult)
| /
|--> Splash --> Main -- Child B (startActivityForResult)
| ^ \
| | Child C (startActivity)
| \
| This Activity is currently skipped if a Notification is started
| while the app is not running or in the background.
我需要在点击通知时实现以下行为:
-
必须维护 Activity 中的状态,因为用户选择了一些食谱来创建购物清单。如果启动一个新的Activity,我相信状态会丢失。
如果应用位于 Main Activity 中,请将其放在最前面,并在代码中告诉我我是从 Notification 到达的。
如果应用位于以
startActivityForResult()
开头的子 Activity 中,我需要在返回主 Activity 之前将数据添加到 Intent,以便它可以正确捕获结果。
如果应用位于以 startActivity()
开头的子 Activity 中,我只需要返回,因为没有其他事情可做(目前可行)。
如果应用程序不在后台,也不在前台(即未运行),我必须启动 Main Activity 并且还知道我是从 Notification 到达的,以便我可以设置设置尚未设置的内容,因为在我当前的设置中,在这种情况下跳过了 Splash Activity。
我在 SO 和其他地方尝试了很多不同的建议,但我未能成功获得上述行为。我也尝试过阅读documentation,但不会变得更聪明,只是一点点。单击我的通知时,我对上述情况的当前情况是:
-
我到达
onNewIntent()
的主要活动。如果应用程序未运行(或在后台),我不会到达这里。这似乎是预期和期望的行为。
我无法在任何子活动中发现我来自通知,因此我无法在这些活动中正确调用setResult()
。 我应该怎么做?
这目前有效,因为通知只是关闭子 Activity,这没关系。
我可以通过使用getIntent()
和Intent.getBooleanExtra()
在通知中设置布尔值来获得onCreate()
中的通知意图。因此,我应该能够使其工作,但我不确定这是最好的方法。 首选的方法是什么?
当前代码
创建通知:
当服务中的 HTTP 请求返回一些数据时,会创建通知。
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setSmallIcon(getNotificationIcon())
.setAutoCancel(true)
.setColor(ContextCompat.getColor(context, R.color.my_brown))
.setContentTitle(getNotificationTitle(newRecipeNames))
.setContentText(getContentText(newRecipeNames))
.setStyle(new NotificationCompat.BigTextStyle().bigText("foo"));
Intent notifyIntent = new Intent(context, MainActivity.class);
notifyIntent.setAction(Intent.ACTION_MAIN);
notifyIntent.addCategory(Intent.CATEGORY_LAUNCHER);
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
/* Add a thing to let MainActivity know that we came from a Notification. */
notifyIntent.putExtra("intent_bool", true);
PendingIntent notifyPendingIntent = PendingIntent.getActivity(context, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(notifyPendingIntent);
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(111, builder.build());
MainActivity.java:
@Override
protected void onCreate(Bundle savedInstanceState)
Intent intent = getIntent();
if (intent.getBooleanExtra("intent_bool", false))
// We arrive here if the app was not running, as described in point 4 above.
...
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
switch (requestCode)
case CHILD_A:
// Intent data is null here when starting from Notification. We will thus crash and burn if using it. Normally data has values when closing CHILD_A properly.
// This is bullet point 2 above.
break;
case CHILD_B:
// Same as CHILD_A
break;
...
@Override
protected void onNewIntent(Intent intent)
super.onNewIntent(intent);
boolean arrivedFromNotification = intent.getBooleanExtra("intent_bool", false);
// arrivedFromNotification is true, but onNewIntent is only called if the app is already running.
// This is bullet point 1 above.
// Do stuff with Intent.
...
在以 startActivityForResult()
开头的子 Activity 中:
@Override
protected void onNewIntent(Intent intent)
// This point is never reached when opening a Notification while in the child Activity.
super.onNewIntent(intent);
@Override
public void onBackPressed()
// This point is never reached when opening a Notification while in the child Activity.
Intent resultIntent = getResultIntent();
setResult(Activity.RESULT_OK, resultIntent);
// NOTE! super.onBackPressed() *must* be called after setResult().
super.onBackPressed();
this.finish();
private Intent getResultIntent()
int recipeCount = getRecipeCount();
Recipe recipe = getRecipe();
Intent recipeIntent = new Intent();
recipeIntent.putExtra(INTENT_RECIPE_COUNT, recipeCount);
recipeIntent.putExtra(INTENT_RECIPE, recipe);
return recipeIntent;
AndroidManifest.xml:
<application
android:allowBackup="true"
android:icon="@mipmap/my_launcher_icon"
android:label="@string/my_app_name"
android:theme="@style/MyTheme"
android:name="com.mycompany.myapp.MyApplication" >
<activity
android:name="com.mycompany.myapp.activities.SplashActivity"
android:screenOrientation="portrait" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.mycompany.myapp.activities.MainActivity"
android:label="@string/my_app_name"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan" >
</activity>
<activity
android:name="com.mycompany.myapp.activities.ChildActivityA"
android:label="@string/foo"
android:parentActivityName="com.mycompany.myapp.activities.MainActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.mycompany.myapp.activities.MainActivity" >
</meta-data>
</activity>
<activity
android:name="com.mycompany.myapp.activities.ChildActivityB"
android:label="@string/foo"
android:parentActivityName="com.mycompany.myapp.activities.MainActivity"
android:screenOrientation="portrait" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.mycompany.myapp.activities.MainActivity" >
</meta-data>
</activity>
...
</manifest>
【问题讨论】:
这里有一个复杂的问题。我怀疑你会得到一个全面的解决方案!也就是说,可以帮助您制定解决方案的一件事是通知也可以触发广播(不仅仅是活动)。您可以利用这一点来使用 BroadcastReceiver 在调用任何活动之前决定如何处理单击。我不会非常依赖基于活动的意图来做你想做的事。 感谢您的提示,我将研究通知的广播部分。如果它的工作方式看起来我可能能够解决一些问题。 但是(推送)通知来自广播接收器。无需从您的通知中启动另一个 BroadcastReceiver。 如果您将活动状态存储在共享首选项中,您可以在创建通知之前访问它。例如,将所有需要的数据(购物清单、上次打开的活动等)存储在onPause()
。
我想我可以尝试在 SharedPreferences 中存储一些状态来让自己的生活更轻松。像这样在 SharedPreferences 中保存上次打开的 Activity 实际上可能会使一些逻辑变得更简单。有没有办法在子 Activity 由于使用 FLAG_ACTIVITY_CLEAR_TOP 标志而关闭时从通知中拦截 Intent?
【参考方案1】:
这么复杂的问题 :D 以下是您应该如何处理这个问题:
在通知中使用 IntentService 而不是
Intent notifyIntent = new Intent(context, MainActivity.class);
现在,每当用户点击通知时,都会调用一个意图服务。
在意图服务中,广播一些东西。
在所有您想要的活动的 OnResume 中注册广播侦听器(对于您在第二阶段创建的广播)并在 OnPause 中取消注册它
到现在为止,只要您在任何活动中并且用户单击通知,您都会被告知没有任何问题并且没有任何活动的重新创建
在您的应用程序类中定义一个公共布尔值。让我们称之为 APP_IS_RUNNING=false;在您的 MainActivity 中,在 OnPause 中将其设为 false,在 OnResume 中将其设为 true;
通过这样做,您可以了解您的应用是否正在运行或处于后台。
注意:如果您想处理更多状态,例如 isInBackground、Running、Destroyed 等...您可以使用枚举或任何您喜欢的方式
您想在应用程序运行时做不同的事情,对吗?因此,在您在第一阶段声明的意图服务中,检查您在应用程序类中定义的参数。 (在我们的示例中,我的意思是 APP_IS_RUNNING)如果它是真的使用广播,否则调用打开您想要的 Activity 的意图。
【讨论】:
当应用程序不在前台运行时,从 IntentService 启动 Acticity(即不是广播)一直运行良好。据我了解,我们必须提供Intent.FLAG_ACTIVITY_NEW_TASK
标志才能以这种方式启动活动,因此当前堆栈和状态将丢失。有什么建议可以解决这个问题?
对不起。你说的对。现在做这个把戏。在您的意图服务中使用这两个标志 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT 然后创建另一个没有 UI 的活动并在打开时自行关闭!并在您的意图中调用。这样,您将返回堆栈中的最后一个活动:D @Krøllebølle
好的,那么从那里开始,为什么不总是保存您的上一个状态/活动/数据/以及您需要在 SystemPreferences 中恢复到以前的状态的所有内容?这样做可以让您获得保存的最新数据,一旦检测到来自通知的新访问,您需要重建视图...
@JBA:是的,你是对的,这样做实际上也简化了我的应用程序中的其他事情。如果用户关闭应用程序并且我想保存某些状态以供以后使用,这也非常有用。【参考方案2】:
哥们,你走错路了。 onActivityResult 不是解决方案。 对此的一个简单答案是使用 Broadcast Receiver
在你的manifest文件中声明一个动作:
<receiver android:name="com.myapp.receiver.AudioPlayerBroadcastReceiver" >
<intent-filter>
<action android:name="com.myapp.receiver.ACTION_PLAY" />
<!-- add as many actions as you want here -->
</intent-filter>
</receiver>
创建广播接收器的类:
public class AudioPlayerBroadcastReceiver extends BroadcastReceiver
@Override
public void onReceive(Context context, Intent intent)
String action = intent.getAction();
if(action.equalsIgnoreCase("com.myapp.receiver.ACTION_PLAY"))
Myactivity.doSomething(); //access static method of your activity
// do whatever you want to do for this specific action
//do things when the button is clicked inside notification.
在你的 setNotification() 方法中
Notification notification = new Notification.Builder(this).
setWhen(System.currentTimeMillis())
.setSmallIcon(R.drawable.no_art).build();
RemoteView remoteview = new RemoteViews(getPackageName(), R.layout.my_notification);
notification.contentView = remoteview;
Intent playIntent = new Intent("com.myapp.receiver.ACTION_PLAY");
PendingIntent playSwitch = PendingIntent.getBroadcast(this, 100, playIntent, 0);
remoteview.setOnClickPendingIntent(R.id.play_button_my_notification, playSwitch);
//this handle view click for the specific action for this specific ID used in broadcast receiver
现在当用户点击通知中的按钮时,广播接收r会捕捉到该事件并执行操作。
【讨论】:
【参考方案3】:这就是我最终要做的事情。这是一个可行的解决方案,应用程序状态、子活动等的每一种情况都经过测试。进一步的 cmet 非常感谢。
创建通知
通知仍然像原始问题一样创建。我尝试按照@Smartiz 的建议使用带有广播的 IntentService。这在应用程序运行时工作正常;注册的子活动接收广播,我们可以从那时起做我们喜欢的事情,比如照顾状态。然而,问题是当应用程序没有在前台运行时。然后我们必须使用 Intent 中的标志 Intent.FLAG_ACTIVITY_NEW_TASK
从 IntentService 广播(Android 需要这个),因此我们将创建一个新堆栈,事情开始变得混乱。这可能可以解决,但我认为使用 SharedPreferences 或其他人指出的类似东西更容易保存状态。这也是一种更有用的存储持久状态的方法。
因此,通知就像以前一样简单地创建:
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setSmallIcon(getNotificationIcon())
.setAutoCancel(true)
.setColor(ContextCompat.getColor(context, R.color.my_brown))
.setContentTitle(getNotificationTitle(newRecipeNames))
.setContentText(getContentText(newRecipeNames))
.setStyle(new NotificationCompat.BigTextStyle().bigText("foo"));
Intent notifyIntent = new Intent(context, MainActivity.class);
notifyIntent.setAction(Intent.ACTION_MAIN);
notifyIntent.addCategory(Intent.CATEGORY_LAUNCHER);
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
/* Add a thing to let MainActivity know that we came from a Notification.
Here we can add other data we desire as well. */
notifyIntent.putExtra("intent_bool", true);
PendingIntent notifyPendingIntent = PendingIntent.getActivity(context, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(notifyPendingIntent);
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(111, builder.build());
保存状态
在需要保存状态的子活动中,我只需将我需要的内容保存到 onPause()
中的 SharedPreferences。因此,该状态可以在以后需要的任何地方重用。这也是一种以更通用的方式存储状态的非常有用的方法。我没有想到它,因为我认为 SharedPreferences 是为 preferences 保留的,但它可以用于任何事情。我希望我早点意识到这一点。
打开通知
现在,当打开通知时,会发生以下情况,具体取决于应用的状态以及打开/暂停的子 Activity。请记住,使用的标志是Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP
。
A.子活动
-
在前面运行:子 Activity 已关闭,适用状态使用
onPause
中的 SharedPreferences 保存,并且可以在 onCreate
或主 Activity 中的任何位置获取。
应用在后台:同样的行为。
应用程序在后台,但被操作系统杀死(使用adb shell
测试:此时没有堆栈,因此 MainActivity 已打开。但是,应用程序处于脏状态,所以我恢复了该意图将传入数据返回到初始屏幕并返回主 Activity。当用户关闭子 Activity 时,状态再次保存在 onPause
中,并且可以在主 Activity 中获取。
B.主要活动
-
跑在最前面:Intent 被
onNewIntent
捕获,一切都是金色的。做我们想做的事。
应用在后台:同样的行为。
应用程序在后台,但被操作系统杀死(使用adb shell
测试:应用程序处于脏状态,因此我们将 Intent 恢复为启动屏幕/加载屏幕并返回主 Activity。李>
C.应用根本没有运行
这真的和Android在后台杀死应用程序以释放资源一样。只需打开主 Activity,恢复到启动屏幕以进行加载,然后返回主 Activity。
D.飞溅活动
用户在按下 Notification 的同时处于启动 Activity/加载 Activity 的可能性不大,但理论上是可能的。如果用户这样做,StrictMode 会抱怨关闭应用程序时有 2 个主要活动,但我不确定它是否完全正确。无论如何,这是高度假设的,所以我现在不会花太多时间。
我不认为这是一个完美的解决方案,因为它需要在这里进行一些编码,那里进行一些编码,并且如果应用程序处于脏状态,则需要来回恢复 Intent,但它可以工作。非常感谢您的评论。
【讨论】:
太好了,我现在的问题是如何使用 firebase 通知复制它以上是关于无论应用程序状态如何,都可以从通知中正确启动 Activity的主要内容,如果未能解决你的问题,请参考以下文章
从 vscode 调试启动时,如何使 Firefox 正确恢复其状态?