从其他应用接收简单数据(通过共享菜单)- 修复 API 问题
如果您在 API Level 21 (Lollipop) 之前的版本中使用 Google 文档中的分享文本方法,文本将无法正确分享到您的应用。本文将介绍这个问题以及如何解决它。
引言
在开发一个用于接收文本并将其保存到用户数据库的应用时,我偶然发现了 Android Intent API 中的一个漏洞,这个 API 本应允许其他应用将文本分享到您的应用。
摘要
我正尝试获取用户通过“分享”菜单发送的数据。在这种情况下,我将使用基本的 Android 网页浏览器选择文本,然后将其分享到我的应用。
问题摘要
当用户第一次分享文本时,我的应用会按预期获取文本并通过 Log.d()
显示它——请参见下面的代码中的 handleSendText()
方法。
然而,在此之后,即使用户已经在网页浏览器中选择了新文本并将其分享到我的应用,我**仍然会获取到原始文本**,即用户之前选择的值。
提问
如何重置 Intent(或发送到 Intent 的值),以便在第一次之后我能够获取用户选择的新文本?
详细说明
我的应用有一个 MainActivity
,我遵循了 Google 的文档:
https://developer.android.com.cn/training/sharing/receive.html (在新标签页中打开)。
在我的 MainActivity
中,代码如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
// when the other app Shares text it is placed as a text/plan mime type
// on the intent so we can then retrieve that text off the incoming intent
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
handleSendText(intent, "onCreate"); // Handle text being sent
}
}
}
@Override
public void onResume(){
super.onResume();
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
handleSendText(intent, "onResume"); // Handle text being sent
}
}
}
void handleSendText(Intent intent, String callingMethodName) {
String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
if (sharedText != null) {
Log.d("MainActivity", "sharedText : " +
sharedText + " called from : " + callingMethodName);
}
}
}
我的 AndroidManifest
文件中活动的过滤器部分如下:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
带有屏幕截图和日志的演练
**注意:** 请注意,我在应用中也实现了 onResume()
,以确保我不仅在调用 onCreate()
时获取 Intent
,因为 MainActivity
的 onCreate()
仅在应用启动时调用。
启动浏览器并获取文本“hurricane”
选择要分享的应用(我们的测试应用)。
查看日志,并注意到 onCreate()
和 onResume()
被调用,值为“hurricane”。
再次返回浏览器以分享更多文本……
选择一个新词“Atlantic”进行分享。
额外说明:这次当我们点击“分享”链接时,Android MenuChooser
不会显示,而是自动重新打开 GrabText
。我发现这个行为有点奇怪。
请注意,Intent 的文本仍然是“hurricane”的值。您可以看到 logcat 中有两条新条目。
尝试的解决方法
销毁 MainActivity & 应用
我发现可以通过覆盖 onPause()
并调用 finish()
来完全销毁应用(从而关闭整个应用),这似乎有效。
由于 MainActivity
和应用被销毁,那么下次尝试从浏览器分享文本时,系统会再次显示“分享”菜单,并允许我选择 GrabText
,并且每次都能正确地从 Intent
中检索新文本。
不幸的副作用
然而,这种解决方案存在不幸的副作用。如果您实现了这个解决方案,并且需要向用户显示一个对话框,那么 onPause()
将被调用,您的应用将被销毁。这不好。
另外,每次您切换离开应用时,onPause()
都会被调用,您的应用将被销毁。
此外,系统本身可能会认为内存不足而暂停您的应用,然后当然您的应用将被销毁。这些都不是好方法,所以我一直在寻找一个解决方法。
解决方法尝试:覆盖 onNewIntent()
在搜索关于为什么第一次之后文本总是错误的答案时,有人建议我在 MainActivity
中添加一个 @Override onNewIntent()
,然后我就可以获取新文本了。我尝试按照以下代码添加:
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.d("MainActivity", "onNewIntent()...");
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
handleSendText(intent, "onNewIntent"); // Handle text being sent
}
}
}
添加了这段代码并运行,在尝试复制然后第二次复制新词后,我仍然在 logcat 中看到以下内容,这表明我仍然没有捕获到新文本:
此外,由于我在 onNewIntent()
中有日志语句,我可以看到 onNewIntent()
根本没有被调用。
开发人员设置:“不保留活动”
我注意到每次 Intent
到来时,onCreate()
和 onResume()
都会被调用。这意味着 MainActivity
应该已经完全卸载了,因为每次分享文本时都会调用 onCreate()
。这应该能帮助我获取文本,但我想尝试改变一下,看看会发生什么。
我修改了模拟器设置……开发者选项……并关闭了“不保留活动”设置。它之前是开启的(已勾选)。
之后,我再次运行,并覆盖了 onNewIntent()
,但现在日志中只显示了一个 onCreate()
(这很有道理,因为活动仍然加载,并且第二次调用 onCreate()
不会被调用),但仍然没有显示 onNewIntent()
的调用。
在此示例中,我捕获了单词“remnants”。
解决方法尝试:在真实硬件上运行程序
我构建了应用并创建了一个 APK,将其部署到我的三星 Galaxy Core Prime 上,结果相同。onNewIntent()
**从未被调用**。
我更仔细地查看了 Google 关于 onNewIntent
的开发文档,它说明:
onNewIntent(Intent intent) 当活动的 launchMode 设置为“singleTop”或客户端在调用 startActivity(Intent) 时使用了 FLAG_ACTIVITY_SINGLE_TOP 标志时,会调用此方法。
我修改了我的 AndroidManifest.xml 文件,使其如下所示:
<activity android:name=".MainActivity" android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
切换 API Level(Android 版本)
当我做出那个改变时,我实际上还做了另一个改变,它有点无效我的测试,因为我切换了模拟器上的 API Level。我当时运行的是 API LEVEL 15 (Android v4.0.4)。然而,这时,我切换到了 API LEVEL 21 (v5.0 Lollipop) 来看看是否有任何区别。
选定的文本已更改:成功?
在 Android API LEVEL 21 上,每次我在浏览器中选择文本时,Intent
的文本现在都会显示不同的内容。
onNewIntent 永远不会被调用
然而,onNewIntent()
**从未被调用**。即使修改了 Manifest 或更改了 API Level,也从未调用。我从未见过 onNewItent()
被触发。
每次都显示分享菜单
另外,现在(在 API 21 上),每次我选择文本时都会看到“分享”菜单。
然而,当我切换到浏览器时,我还看到了一些有趣的事情。您可以看到列表中有多个活动副本。什么?!
运行时视图的 ListView
还请注意,我实现了 MainActivity
作为 ListView
(可滚动),以便即使没有 logcat 也能看到条目(用于在真实设备上运行)。这使得其他一些问题显而易见:ListView
在每个新显示的活动上都会更新。但实际上,它应该只是追加到原始 Activity
的 ListView
。为什么每次我分享文本时它都会创建一个新的应用/MainActivity
?
创建了多个 GrabText 活动
是的,每次我选择文本时,它都会创建一个新的 GrabText
活动窗口。我以为这可能是因为我设置了 singleTop
,所以我将其移除,但即使在 API LEVEL 21 上移除了 singleTop
后,它们仍然出现。
在 API Level 21 上有效,尝试在 API Level 15 上
现在我看到它有效——在 API 21 上每次提供不同的文本——我决定切换回 API Level 15 模拟器再试一次。
API Level 15:再次测试
我再次启动了另一个运行 API Level 15 的模拟器并运行了应用,即使设置了 singleTop
,值也从未更新。
您可以在 logcat 和更新的 ListView
中看到这一点。
您还可以看到代码的作用完全不同,尽管我没有更改任何内容,因为它在 API level 15 上的运行活动中追加到 ListView
。
解决方法和解决方案
经过一段时间的思考,我得出了一个想法。
传递活动
我决定创建一个传递活动,该活动将从 Intent
中获取文本,然后用它来启动我的 MainActivity
。然后,我将在该传递活动中添加一个覆盖的 onPause()
并调用 finish()
,这样传递活动就会被销毁,并且用户永远不会看到它。
我在代码中将此活动命名为 Transit
,这是该代码的完整列表。它非常简单。
public class transit extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transit);
Intent intent = getIntent();
if (intent != null) {
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
handleReceivedText(intent); // Handle text being sent
}
}
}
}
void handleReceivedText(Intent intent) {
String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
if (sharedText != null) {
Log.d("MainActivity", "sharedText : " + sharedText);
Intent i = new Intent(getApplicationContext(), MainActivity.class);
i.putExtra("SHAREDTEXT", sharedText);
startActivity(i);
}
}
@Override
public void onPause(){
super.onPause();
finish();
}
}
基本工作流程
onCreate()
获取用户分享的文本并将其传递给handleReceivedText()
。handleReceivedText()
从传入的 intent 中检索文本(参见getStringExtra()
),并创建一个新的Intent
,即我的MainActivity()
。它将分享的文本放入MainActivity
的Intent
中并启动MainActivity
。- 由于
MainActivity
将成为最前面的Activity
,Transit Activity
中的onPause()
将被调用,此时我调用finish()
,这将销毁Transit Activity
。
Manifest 更改
您还需要对 manifest 进行更改,以确保 Transit Activity
内存中只有一个实例。这确保了每次用户从调用应用(在本例中为网页浏览器)分享文本时,都会在分享菜单中显示 GrabText
应用。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="us.raddev.grabtext">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<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="us.raddev.grabtext.transit"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
实际效果
从网页浏览器分享文本(分享菜单出现)
选择 GrabText 应用接收文本。
第一次时,请注意文本。
选择不同的文本进行分享,并再次选择 GrabText 应用。
第二次,请注意文本已不同。
现在文本不同了,但存在一些问题。出于某种原因,即使 ListView
的 ArrayAdapter
仍然在内存中,ListView
中也只有一个项目。
解决这个问题是留待以后。
Using the Code
您可以从此文章下载源代码,解压缩它,然后将主文件夹放到您的硬盘上,并使用最新版本的 Android Studio(2015 年 12 月 v1.5.1 build xxx)打开它,构建并运行以查看所有内容。
历史
- 2016/03/24:代码和文章的首次发布