Introduction
While developing an application to receive text and save it in a database for the user, I stumbled upon a hole in the Android Intent APIs which are supposed to allow other apps to share text with your app.
Summary
I am attempting to get data sent from the user via the Share menu. In this case, I'll use the basic Android web browser to select text and then share it to my app.
Problem Summary
The first time the user shares the text, my app gets the text as expected and displays it via Log.d()
-- see the handleSendText()
method in the code below.
However, each time thereafter, even though the user has selected new text in the web browser and shared it with my app, I still get the original text the user selected (previous value).
Question
How do you reset the Intent (or the value sent on the Intent) so that I can obtain the new text the user has selected after the first time?
Details
My application has a MainActivity
and I've followed the Google docs at
http://developer.android.com/training/sharing/receive.html (loads in new tab).
With code like the following in my 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();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if ("text/plain".equals(type)) {
handleSendText(intent, "onCreate");
}
}
}
@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");
}
}
}
void handleSendText(Intent intent, String callingMethodName) {
String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
if (sharedText != null) {
Log.d("MainActivity", "sharedText : " +
sharedText + " called from : " + callingMethodName);
}
}
}
My AndroidManifest
section for the activity has the filter added like:
<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>
Walk-Thru With Screens and Log
**NOTE:** Please notice that I've implemented the onResume()
in my app also to ensure that I don't only get the Intent
when onCreate()
is called since the MainActivity
's onCreate()
is only called when the app istarts.
Start up the browser and grab the text "hurricane"
Choose the app to share with (our test app).
View the log and notice that onCreate() and onResume() are called and value is 'hurricane'.
Go back to browser again to share more text...
Select a new word, Atlantic, to share.
Extra note: When we click that Share link this time, the Android MenuChooser
doesn't display, instead, it automatically opens GrabText
again. I found that behavior somewhat odd.
Notice that the Intent text still has the value of hurricane. You can see that there are now two new entries in the logcat.
Attempted Workaround Solutions
Destroy MainActivity & App
I have found that I can destroy the app entirely by overriding onPause()
and calling finish()
on my Activity
(thus closing the entire app) and that seems to work.
Since the MainActivity
and the App is destroyed, then the next time you attempt to Share text from the browser, the system displays the Share menu again and allows me to choose GrabText
and the new text is retrieved properly from the Intent
every time.
Unfortunate Side Effects
However, there are unfortunate side effects to that solution. If you implement that solution and you need to display a Dialog box to your user, then onPause()
will get called and your app will be destroyed. That's no good.
Also, every time you switch away from your app, then onPause()
will be called and your app will be destroyed.
Additionally, the system itself may decide that memory is low and pause your app and then of course your app will be destroyed. None of these are great so I wanted to find a workaround.
Workaround Attempt: Override onNewIntent()
While searching for answers to this question about why the text is never right after the first time someone said I should add an @Override onNewIntent()
in my MainActvity
and then I would get the new text. I tried adding it as in the following code:
@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");
}
}
}
Upon adding that code and running and attempting the copy and then second copy of the new word, I still saw the following in logcat indicating that I still hadn't captured the new text:
Furthermore, since I had the log statement in onNewIntent()
I could see that onNewIntent()
was never called.
Dev Setting: Don't Keep Activities
I noticed that each time the Intent
was coming in that onCreate()
and onResume()
were being called. That means MainActivity
should've been unloading entirely, since the onCreate()
was being called every time I shared text. This should've helped me get the text, but I figured I should try changing it up and see what happens.
I altered the emulator Settings...Developer Options... and turned off the "Don't keep activities" setting. It was previously turned on (checked).
After that, I ran it again with the overridden onNewIntent()
but now it shows just the one onCreate()
in the log (which makes sense because the activity is still loaded and onCreate()
isn't called the second time) but still does not show the onNewIntent()
call.
In this sample, I captured the word "remnants".
Workaround Attempt: Run Program On Real Hardware
I built the app and created an APK and deployed it to my Samsung Galaxy Core Prime and I ended up with the same results. onNewIntent()
is never called.
I looked more closely at the Google Dev Docs on onNewIntent
and it states:
onNewIntent(Intent intent) This is called for activities that set launchMode to "singleTop" in their package, or if a client used the FLAG_ACTIVITY_SINGLE_TOP flag when calling startActivity(Intent).
I altered my AndroidManifest.xml so that it looked like the following:
<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>
Switched API LEVELS (Android Version)
When I made that change, I actually made another change too and it somewhat invalidated my tests because I switched API Levels on my emulator. I was running API LEVEL 15 (Android v4.0.4). However, at this point, I switched to API LEVEL 21 (v5.0 Lollipop) to see if there were any differences.
Selected Text Did Change: Success?
On Android API Level 21, the Intent
text was now coming in different each time I selected text in the browser.
onNewIntent Is Never Called
However, onNewIntent()
is NEVER called. Not even with the change to the Manifest or the API Level change. I don't ever see onNewItent()
fire.
Share Menu Displayed Every Time
Also, now (on API 21), I see the Share menu every time I select text.
However, I also see an interesting thing when I switch to the browser. You can see multiple copies of the Activity in the list. What?!
ListView For Run-time View
Notice also that I implemented the MainActivity
as a ListView
(scrollable) so I could see the entries even without logcat (for running on real device). That made something else apparent: that the ListView
was being updated on each newly shown Activity. But really, it should just be appending to the original Activity
's ListView
. Why does it create a new app / MainActivity
every time I share text?
Creates Numerous GrabText Activities
Yes, now it creates a new GrabText
Activity window each time I select text. I thought maybe that was because I had the singleTop
set so I removed it but they still appear even after removing singleTop
on API LEVEL 21.
Works On API Level 21, Try On API Level 15
Now that I saw it work -- provide a different text each time on API 21 -- I decided to switch back to API Level 15 emulator and try it.
API Level 15: Test Again
I started my other emulator running API Level 15 again and ran the app and even with singleTop
set the value is never updated.
You can see this in the logcat and on the updated ListView
:
You can also see that the code acts completely different, though I've not changed anything since it appends to the ListView
of the one running Activity on API level 15.
Workaround and Solution
After thinking about this for a while, I came up with an idea.
Pass-Thru Activity
I decided to create a pass-thru activity which would grab the text off of the Intent
and then use it to start my MainActivity
. I would then add override onPause()
in this pass-thru activity and call finish()
so that the pass-thru activity would be destroyed and never seen by the user.
I've named this activity Transit
in my code and here is the entire listing for that code. It's very simple.
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);
}
}
}
}
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();
}
}
Basic Flow of How It Works
onCreate()
grabs the text the user has shared and passes it to handleReceivedText()
handleReceivedText()
retrieves the text from the incoming intent (see getStringExtra()
) and creates a new Intent
which is my MainActivity()
. It places the shared text on the MainActivity
Intent
and starts MainActivity
. - Since the
MainActivity
will become the front most Activity
, onPause()
will be called in the Transit Activity
and that's when I call finish()
which destroys the Transit Activity
.
Manifest Change
There is a manifest change you have to make also to insure that the Transit Activity
only ever has one instance in memory. This ensures that each time the user shares text from the calling app (web browser in our case) that it will display the GrabText
app in the share menu each time.
="1.0"="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>
Here It Is In Action
Share Text from web browser (share menu appears)
Choose the GrabText app to receive the text.
Notice the text the first time through.
Choose different text to share and select GrabText
app again.
Notice the text is different the second time.
Now the text is different, but there is somewhat of an issue. For some reason, even though the ListView
's ArrayAdapter
is still in memory, we only see the one item in the ListView
.
Solving that is for another day.
Using the Code
You can download the source from this article, unzip it and drop the main folder on your hard drive and open it with the latest version of Android Studio (December 2015 v1.5.1 build xxx), build and run to see it all work.
History
- 03/24/2016: First release of code and article