Android Web Intent Issue - android

G'day,
Disclaimer: I'm not an Android dev, I'm QAing an Android app with the issue I'm describing. The technical terms I use to describe this issue might be wrong.
I'm testing an Android app that describes in its manifest that it can handle web intents with the address of type https://www.example.com/app/(.*). The way it should handle these URLs is that it gets the first match group $1 and sends a request to https://api.example.com/$1 and if the response is a HTTP200, it renders the response within the app. If not, it should open the URL in any browser app the user has installed on their device (by sending an intent to open the URL). If the user has no browser apps installed on their device, we show an error prompt saying they don't have a browser installed which can handle this URL.
Now, this works fine except when the user marks this app as the default to handle URLs like https://www.example.com/app/(.*) when it first tries to open a URL like https://www.example.com/app/(.*). Then, even if the user has browser apps installed on their system, when they open a link that needs to be opened in a browser, the only option seems to be the our original app and we have to show the error message (as it seems like there are no other browser apps installed on the system which can handle this URL).
One way to tackle this is to show a message asking the user to clear the defaults for this app when we encounter a URL that needs to be opened in a browser app but the only option is our own app — but this is terrible UX. Is there another work-around for this issue?
Sample code to understand the issue: https://gist.github.com/GVRV/5879fcf0b1838b495e3a2151449e0da3
Edit 1: Added sample code link

To solve this problem and keep the systems default handling of intents you need 2 additional activities and 1 <activity-alias>:
Create a new invisible empty Activity. I called it IntentFilterDelegationActivity. This activity is responsible to receive URL intents from the activity-alias (defined in the next step). Manifest:
<activity
android:name=".intent_filter.IntentFilterDelegationActivity"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleInstance"
android:noHistory="true"
android:taskAffinity=""
android:theme="#style/Theme.Transparent"/>
Code:
public class IntentFilterDelegationActivity extends Activity
{
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
findAndStartMatchingActivity(getIntent());
finish();
}
}
Create an <activity-alias>. The activity alias is responsible to delegate your URL intents to your IntentFilterDelegationActivity. Only a Manifest entry is needed:
<activity-alias
android:name="${packageName}.IntentFilterDelegation"
android:enabled="true"
android:exported="true"
android:targetActivity=".intent_filter.IntentFilterDelegationActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http" android:host="www.example.com"/>
</intent-filter>
</activity-alias>
Now you are able to do the trick: You can deactivate the activity-alias before you launch your own URL intent and activate the alias after the launch. This causes android that your app won't be listed as app which can handle the URL intent. To implement the activation and deactivation you need an additional Activity. I called it ForceOpenInBrowserActivity.
Manifest:
<activity
android:name=".activity.ForceOpenInBrowserActivity"
android:excludeFromRecents="true"
android:launchMode="singleInstance"
android:noHistory="true"
android:taskAffinity=""
android:theme="#style/Theme.Transparent"/>
Code:
public class ForceOpenInBrowserActivity extends Activity
{
public static final String URI = IntentUtils.getIntentExtraString(ForceOpenInBrowserActivity.class, "URI");
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Uri uri = fetchUriFromIntent();
if (uri != null)
{
startForcedBrowserActivity(uri);
}
else
{
finish();
}
}
#Nullable
private Uri fetchUriFromIntent()
{
Uri uri = null;
Intent intent = getIntent();
if (intent != null)
{
uri = intent.getParcelableExtra(URI);
}
return uri;
}
private void startForcedBrowserActivity(Uri uri)
{
disableActivityAlias(this);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// After starting another activity, this activity will be destroyed
// android:noHistory="true" android:excludeFromRecents="true"
startActivity(intent);
}
/**
* Re-enable intent filters when returning to app.
* Note: The intent filters will also be enabled when starting the app.
*/
#Override
protected void onStop()
{
super.onStop();
enableActivityAlias();
}
public void disableActivityAlias()
{
String packageName = getPackageName();
ComponentName componentName = new ComponentName(packageName, packageName + ".IntentFilterDelegation"); // Activity alias
getPackageManager().setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
}
public void enableActivityAlias()
{
String packageName = getPackageName();
ComponentName componentName = new ComponentName(packageName, packageName + ".IntentFilterDelegation");
getPackageManager().setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
}
}
Now you can send any URL intent which must be opened in an external browser to the ForceOpenInBrowserActivity:
#NonNull
public static Intent createForceBrowserIntent(Context context, #NonNull Uri uri)
{
Intent intent = new Intent(context, ForceOpenInBrowserActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(ForceOpenInBrowserActivity.URI, uri);
return intent;
}

if website https://www.example.com/ is under your supervision, you could change the logic and use an unique schema like example://app/(.) to handle your case. The website could then use redirection to for its navigation. In this way when you broadcast https://www.example.com/ for action view only browser apps could handle this and your app would be only listening to your custom schema example://app/(.) and wont launch.
Else you could check for default activity and clear it instead of showing an alert.
PackageManager pm = context.getPackageManager();
final ResolveInfo res = pm.resolveActivity(your_intent, 0);
if (res.activityInfo != null && getPackageName()
.equals(res.activityInfo.packageName)) {
pm.clearPackagePreferredActivities("you_package_name");
broadcast your intent
}

Sadly, there is no official solution for this problem (see this SO question).
A workaround is the following:
Use PackageManager.queryIntentActivities(), modify the result to not include your app and show it in a custom chooser dialog.
If you don't want your users to choose a browser every time, you can manage a custom default inside your app.
If you control the domain, there is a cleaner workaround:
Lets say your url is http://www.example.com. Your Android IntentFilter should listen for that schema. Now you create a second schema, e.g. http://web.example.com, which displays the same content as the normal url. If you want to redirect to the web from your app, use the second schema. Everywhere else, use the first one.
Note that you should not use a custom schema like example://, because this will cause problems if your app is not present.

Related

Reading a message stored on NDEF tag. Android

I was trying to find a working example of how it is possible to read a message stored on a NDEF tag within the app's active Activity. By far the best I have is such a code:
public class Activity1_3_1_1 extends AppCompatActivity {
private Button done;
NfcAdapter adapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity1_3_1_1);
done = findViewById(R.id.button5);
done.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
switchActivityTo1();
}
});
}
private void switchActivityTo1() {
Intent switchActivityIntent = new Intent(this, MainActivity.class);
startActivity(switchActivityIntent);
}
#Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {
adapter = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); // get the detected tag
Parcelable[] msgs =
intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
NdefRecord firstRecord = ((NdefMessage) msgs[0]).getRecords()[0];
byte[] payload = firstRecord.getPayload();
int payloadLength = payload.length;
int langLength = payload[0];
int textLength = payloadLength - langLength - 1;
byte[] text = new byte[textLength];
System.arraycopy(payload, 1 + langLength, text, 0, textLength);
Toast.makeText(getApplicationContext(), new String(text), Toast.LENGTH_LONG).show();//display the response on screen
}
}
}
And the Manifest file:
...
<uses-permission android:name="android.permission.NFC"/>
<uses-feature android:name="android.hardware.nfc"/>
...
<activity
android:name=".Activity1_3_1_1"
android:exported="true"
android:alwaysRetainTaskState="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
The problem is that NFC Service is launching instead of app's onNewIntent() method.
And it is a problem for me figuring out am messed up at Manifest file (because one of solutions was to modify Manifest file in order to NFC Service won't launch) or it is problem inside the Activity code itself. Or, perhaps, both.
Waiting for your solutions.
So the normal pattern for NFC in Android is:-
1)When you App is not running an you want it started when a certain type of NFC Tag is presented to the device then you put your intent-filters in the manifest. Your App then gets started and passed an Intent that you need to process in your onCreate method using getIntent()
2a)Your App is already running in the foreground then you use enableForegroundDispatch, giving it a pending Intent on what you want to be notified about, this is then processed in onNewIntent when your App is restarted (paused and resumed) to receive the Intent.
onNewIntent won't get invoked by any manifest entry.
or
2b)Your App is already running in the foreground then you use enableReaderMode which is a better replacement for enableForegroundDispatch, you then process the Tag in onTagDiscovered which is in a separate thread.
How to process the Intent received via pattern 1 and 2a is the same, just they need to be called from the correct path in the code that matches the method that triggered the Intent i.e in onCreate or in onNewIntent
Check out Empty NFC Tag read and write Android Application. Mobile own message returning while scanning Empty Tag but Application not working? for an example of how to use Manifest and enableForeGroundDispatch
The are also plenty examples of using enableReaderMode on Stackoverflow as well.

Why is "Dynamic Links" created dynamically by android app not opening app directly?

Problem:
Why is "Short dynamic links" created programatically wont open/launch the app directly?
But link created at console, shows only one prompt to choose browser then app launches directly.
I want to launch app directly when user clicks the dynamic url created dynamically by android app.
When clicking dynamic short link created dynamically by android app the following things happen,
1.Option to open in browser shows
2.The browser opens,shows a loading dialog box
3.Again shows option to open in browser(this time app shows in the more options area)
4.Clicking browser opens website,clicking app lauches app but the deep link is lost.
Any Help would be great.
Manifest.xml
...
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="notifika.tupio.me"
android:scheme="https" />
</intent-filter>
...
Link Generation
public void createDynamicLink(final Context context, final SingleNotification notification){
final Uri[] mInvitationUrl = {null};
String link = "https://notifika.tupio.me/?public=" + notification.getTag();
FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLink(Uri.parse(link))
.setDynamicLinkDomain("notifika.page.link")
.setAndroidParameters(
new DynamicLink.AndroidParameters.Builder("me.tupio.notifika")
.build())
.setGoogleAnalyticsParameters(
new DynamicLink.GoogleAnalyticsParameters.Builder()
.setSource("In-App")
.setMedium("social")
.setCampaign("Word-Word")
.build())
.setSocialMetaTagParameters(
new DynamicLink.SocialMetaTagParameters.Builder()
.setTitle("Notifika")
.setDescription("Sent and Receive Notification like this.Download the app now")
.build())
.buildShortDynamicLink()
.addOnSuccessListener(new OnSuccessListener<ShortDynamicLink>() {
#Override
public void onSuccess(ShortDynamicLink shortDynamicLink) {
mInvitationUrl[0] = shortDynamicLink.getShortLink();
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, notification.getTitle()+"\n"+notification.getMessage().substring(0, Math.min(notification.getMessage().length(), 100)) + "..." +context.getResources().getString(R.string.share_link_desc)+"\n"+mInvitationUrl[0]);
context.startActivity(Intent.createChooser(intent, "Share"));
}
});
}
MainActivity.class
FirebaseDynamicLinks.getInstance()
.getDynamicLink(getIntent())
.addOnSuccessListener(this, new OnSuccessListener<PendingDynamicLinkData>() {
#Override
public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) {
// Get deep link from result (may be null if no link is found)
Uri deepLink = null;
if (pendingDynamicLinkData != null) {
deepLink = pendingDynamicLinkData.getLink();
Log.v("Deeplink",deepLink.toString());
}
//
// If the user isn't signed in and the pending Dynamic Link is
// an invitation, sign in the user anonymously, and record the
// referrer's UID.
//
if (deepLink != null
&& deepLink.getBooleanQueryParameter("public",false)) {
String referrerUid = deepLink.getQueryParameter("public");
Log.v("Deeplink", referrerUid);
Toast.makeText(getApplicationContext(),referrerUid,Toast.LENGTH_LONG).show();
}
}
});
Followed this guide.
You'll need to add an intent filter for your page.link/app.goo.gl domain as well, so the deep link goes straight to your app. See: https://firebase.google.com/docs/dynamic-links/android/receive#app_links

Bring Android app to front of other windows

I have an Android app that launches automatically when a USB stick is plugged into the phone. This works well and as expected.
The trouble is, on many android phones, the "My Files" app launches as well when it sees a usb stick inserted. The "My Files" app comes up after my app, so it covers up my app window.
Is there a way to either bring my app window in front of the My Files window or get it to come up after the My Files window?
I've tried different priority values as mentioned in to bring app to front on receive of a call but it doesn't seem to help.
The receiver code I am using is:
public class MediaReceiver extends BroadcastReceiver {
private static final String TAG = MediaReceiver.class.getSimpleName();
#Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
LogHelper.log(TAG, "Intent received, action = " + action);
AppEx appEx = (AppEx)context.getApplicationContext();
if (action.equals("android.hardware.usb.action.USB_DEVICE_ATTACHED")) {
if(appEx.isActivityVisible()) {
context.sendBroadcast(new Intent(BaseActivity.ACTION_STICK_ATTACHED));
} else {
Intent i = new Intent(context, SplashActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
}
appEx.stickAttached();
} else if (action.equals("android.hardware.usb.action.USB_DEVICE_DETACHED")) {
appEx.stickDetached();
context.sendBroadcast(new Intent(BaseActivity.ACTION_STICK_DETACHED));
}
}
}
And, in my AndroidManifest.xml, I have:
<receiver android:name="com.android.myApp.receivers.MediaReceiver">
<intent-filter android:priority="2147483647">
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
<action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />
</intent-filter>
</receiver>

Callback from BroadcastReceiver beetwen Apps?

I want to allow other Apps to integrate with mine and I'm writing a dummy "consumer" app but I cant achieve to return a callback to notify the "consumer" app if everything went well.
So my DUMMY_APP has a simple layout with 2 buttons a success call, and a call with a wrong EXTRA param.
To make DUMMY_APP to call MAIN_APP I use sendBroadcast
// MainActivity class
private static final String REQUIRED_ACTION = "com.basetis.afr.intent.action.INIT_TEXT_FLOW";
onCreate....
Button btnSuccess = (Button)findViewById(R.id.button_success_call);
btnSuccess.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
i.setAction(REQUIRED_ACTION);
i.putExtra(Intent.EXTRA_TEXT, textToBeRead);
sendBroadcast(i);
}
});
So MAIN_APP has the corresponding BroadcastReceiver that is receiving fine.
// BlinkingReadReceiver class
private static final String CALLBACK_CALL_AFR_ACTION = "com.basetis.afr.intent.action.CALLBACK_CALL_AFR_ACTION";
#Override
public void onReceive(Context context, Intent intent) {
Intent i = new Intent();
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Log.d(TAG, "SUCCESS send callback");
i.setAction(CALLBACK_CALL_AFR_ACTION);
i.putExtra(CALL_AFR_SUCCESS_EXTRA, CALL_AFR_SUCCESS_EXTRA_DESC);
i.setType("text/plain");
context.sendBroadcast(i);
}
So the DUMMY_APP BroadcastReceiver never receive nothing :(
So I configured Manifests like that:
DUMMY_APP
<receiver android:name=".MainBroadcastReceiver" android:enabled="true">
<intent-filter>
<action android:name="com.basetis.afr.intent.action.CALLBACK_CALL_AFR_ACTION"></action>
</intent-filter>
</receiver>
MAIN_APP
<receiver android:name=".BlinkingReadReceiver" android:enabled="true">
<intent-filter>
<action android:name="com.basetis.afr.intent.action.INIT_TEXT_FLOW"></action>
</intent-filter>
</receiver>
Sometimes I receive this error (afrsender is de DUMMY_APP) but seems sort of random...
Performing stop of activity that is not resumed: {com.basetis.afrsender.afrsender/com.basetis.afrsender.afrsender.MainActivity}
java.lang.RuntimeException: Performing stop of activity that is not resumed
Any suggestions about how to achieve this two way App communication?
Thank you very much.
As stated in the document
Starting from Android 3.1, the system's package manager keeps track of applications
that are in a stopped state and provides a means of controlling their launch from
background processes and other applications.
That means that till the app is not started manually by the user your app will be in force stop state and it won't receive any broadcast.
That's why your dummy app is not receiving and broadcast sent by main app.
Check here for more reference

Android return from browser to app

I have option in my app to start browser and load imdb website.
I'm using ActionView for this.
Intent intent1 = new Intent(android.content.Intent.ACTION_VIEW,
Uri.parse(website));
try {
activity.startActivity(intent1);
} catch (Exception e) {
Toast.makeText(activity, R.string.no_imdb, Toast.LENGTH_SHORT)
.show();
}
The problem occurs when I tap on back button.
When default browser app is launched everything is ok.
When Opera Mini app is launched, when I tap on back button, it seems like my app receives two back actions, and finish my current activity.
How to prevent this?
Try starting the intent in a new task:
intent1.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Or
intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Please add this code to your android manifest for activity that you need return
<activity
android:name="YourActivityName"
android:launchMode="singleTask">
<intent-filter>
<action android:name="schemas.your_package.YourActivityName" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.ALTERNATIVE" />
</intent-filter>
</activity>
and add this to your web page
click to load app
because only one app has this action name (schemas.your_package.YourActivityName) on your phone, web page directly return to app
Also You can Use Airbnb DeepLink lib
Example
Here's an example where we register SampleActivity to pull out an ID from a deep link like example://example.com/deepLink/123. We annotated with #DeepLink and specify there will be a parameter that we'll identify with id.
#DeepLink("example://example.com/deepLink/{id}")
public class SampleActivity extends Activity {
#Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent.getBooleanExtra(DeepLink.IS_DEEP_LINK, false)) {
Bundle parameters = intent.getExtras();
String idString = parameters.getString("id");
// Do something with idString
}
}
}

Categories

Resources