65.9K
CodeProject 正在变化。 阅读更多。
Home

从其他应用接收简单数据(通过共享菜单)- 修复 API 问题

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (4投票s)

2016年3月24日

CPOL

8分钟阅读

viewsIcon

17419

downloadIcon

317

如果您在 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,因为 MainActivityonCreate() 仅在应用启动时调用。

启动浏览器并获取文本“hurricane”

grab text hurricane

选择要分享的应用(我们的测试应用)。

Share with GrabText app

查看日志,并注意到 onCreate()onResume() 被调用,值为“hurricane”。

view log see values

再次返回浏览器以分享更多文本……

back to browser

选择一个新词“Atlantic”进行分享。

select new word: atlantic

额外说明:这次当我们点击“分享”链接时,Android MenuChooser 不会显示,而是自动重新打开 GrabText。我发现这个行为有点奇怪。

请注意,Intent 的文本仍然是“hurricane”的值。您可以看到 logcat 中有两条新条目。

intent text is still hurricane

尝试的解决方法

销毁 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 中看到以下内容,这表明我仍然没有捕获到新文本:

still doesn't capture new word

此外,由于我在 onNewIntent() 中有日志语句,我可以看到 onNewIntent() 根本没有被调用。

开发人员设置:“不保留活动”

我注意到每次 Intent 到来时,onCreate()onResume() 都会被调用。这意味着 MainActivity 应该已经完全卸载了,因为每次分享文本时都会调用 onCreate()。这应该能帮助我获取文本,但我想尝试改变一下,看看会发生什么。

我修改了模拟器设置……开发者选项……并关闭了“不保留活动”设置。它之前是开启的(已勾选)。
don't keep activities

之后,我再次运行,并覆盖了 onNewIntent(),但现在日志中只显示了一个 onCreate()(这很有道理,因为活动仍然加载,并且第二次调用 onCreate() 不会被调用),但仍然没有显示 onNewIntent() 的调用。
在此示例中,我捕获了单词“remnants”。

another attempted capture

解决方法尝试:在真实硬件上运行程序

我构建了应用并创建了一个 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 上),每次我选择文本时都会看到“分享”菜单。
然而,当我切换到浏览器时,我还看到了一些有趣的事情。您可以看到列表中有多个活动副本。什么?!

multiple copies of grabtext

运行时视图的 ListView

还请注意,我实现了 MainActivity 作为 ListView(可滚动),以便即使没有 logcat 也能看到条目(用于在真实设备上运行)。这使得其他一些问题显而易见:ListView 在每个新显示的活动上都会更新。但实际上,它应该只是追加到原始 ActivityListView。为什么每次我分享文本时它都会创建一个新的应用/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 中看到这一点。

logcat onResume

append to ListView via api15

您还可以看到代码的作用完全不同,尽管我没有更改任何内容,因为它在 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();
    }
}

基本工作流程

  1. onCreate() 获取用户分享的文本并将其传递给 handleReceivedText()
  2. handleReceivedText() 从传入的 intent 中检索文本(参见 getStringExtra()),并创建一个新的 Intent,即我的 MainActivity()。它将分享的文本放入 MainActivityIntent 中并启动 MainActivity
  3. 由于 MainActivity 将成为最前面的 ActivityTransit 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>

实际效果

从网页浏览器分享文本(分享菜单出现)

android web browser

选择 GrabText 应用接收文本。

Share menu appears

第一次时,请注意文本。

choose GrabText app.

选择不同的文本进行分享,并再次选择 GrabText 应用。

第二次,请注意文本已不同。

first time text

现在文本不同了,但存在一些问题。出于某种原因,即使 ListViewArrayAdapter 仍然在内存中,ListView 中也只有一个项目。

解决这个问题是留待以后。

Using the Code

您可以从此文章下载源代码,解压缩它,然后将主文件夹放到您的硬盘上,并使用最新版本的 Android Studio(2015 年 12 月 v1.5.1 build xxx)打开它,构建并运行以查看所有内容。

历史

  • 2016/03/24:代码和文章的首次发布
© . All rights reserved.