Possible to prevent app launch by NFC intent from within an app? - android

I have an app for which the requirements are to launch it on detection of a non-NDEF NFC tag, so I'm using the TECH_DISCOVERED filter on my main activity to do so:
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
This works fine, however some users complain that their phone case doubles as a holder for their credit cards / smart cards and hence the app is unintentionally launching when they close their phone case. These users don't want to have to disable the device NFC setting (and that can't be done programmatically) so my question is: is it possible to programmatically stop an app launching by NFC intent from within that app?
The best idea I can come up with is to have the NFC intent launch a non-UI Activity (one that doesn't call setContentView) and have this check if some persistent flag has been set (by a UI control in the main activity) and if the flag is set, do not launch the main activity.
Is there an easier/more elegant solution?

The app could simply try to check whether the "discovered" tag belongs to the app (i.e. the datastructure is as expected, resp. the TagType is as expected), and if not stop again. Whether you make that visible to the use or not is up to you ...

My solution to this was to launch a headless (invisible) activity via the NFC intent and use a shared preference (set by a UI switch via the main activity) to determine whether to launch the main activity.
AndroidManifest.xml:
<activity android:name="com.mypackage.NFCLaunchActivity" android:theme="#android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
<meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="#xml/nfc_tech_filter" />
</activity>
res/nfc_tech_filter.xml:
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.NfcA</tech>
</tech-list>
</resources>
MainActivity.java:
public static String SETTINGS_NAME = "settings";
public static String shouldLaunchByNFC = "launchWithNfc";
// Call on changing UI state
protected void setShouldLaunchByNFC(boolean enableLaunch) {
setSettingBoolean(this, shouldLaunchByNFC, enableLaunch);
}
// Call to set initial UI state
protected boolean getShouldLaunchByNFC() {
return getSettingBoolean(this, shouldLaunchByNFC, true);
}
public static void setSettingBoolean(Activity activity, String name, boolean value){
SharedPreferences settings = activity.getSharedPreferences(SETTINGS_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean(name, value);
editor.commit();
}
public static boolean getSettingBoolean(Activity activity, String name, boolean defaultValue){
SharedPreferences settings = activity.getSharedPreferences(SETTINGS_NAME, MODE_PRIVATE);
return settings.getBoolean(name, defaultValue);
}
NFCLaunchActivity.java:
import static com.mypackage.MainActivity.getSettingBoolean;
import static com.mypackage.MainActivity.shouldLaunchByNFC;
public class NFCLaunchActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView explicitly omitted
boolean launchWithNfc = getSettingBoolean(this, shouldLaunchByNFC, true);
if(launchWithNfc){
Context context = this.getApplicationContext();
Intent intent = new Intent();
intent.setClassName(context, context.getPackageName() + ".MainActivity");
context.startActivity(intent);
}
finish();
}
}

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.

Checking first run of Activity, and suppressing another activity in Android

I'm making a launcher application with another settings activity to tweak the launcher.
Now, i don't want the launcher to be displayed as elligible upon pressing the home button until the user has not set it up up first (they will be asked to do that once the app downloads via notifications) through the settings activity.
So, can i suppress my launcher activity from running until after first run of application, and if not, then how to know first run of an activity.
PS: I already know how to implement first run of application.
This is based on the "Settings Activity" project created using Android Studio's "Start a new Android Studio project" template. After the project is successfully created, add a new activity class (that would be your launcher activity in your current project); for this example, it is a plain empty activity.
public class HomeScreenActivity extends AppCompatActivity {
}
Then add AndroidManifest.xml entries for that activity:
<activity
android:name=".HomeScreenActivity"
android:enabled="false"
android:label="#string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
Pay attention to android:enabled="false", that is the important part here. That way your launcher activity will be disabled by default. You will change it's state after user goes through the setup process.
In this example, I simply added a SwitchPreference and changed HomeScreenActivity's state based on the user click.
private SwitchPreference prefEnableDisableHomeScreen;
private PackageManager packageManager;
private ComponentName homeScreenComponent;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_general);
setHasOptionsMenu(true);
packageManager = getActivity().getPackageManager();
homeScreenComponent = new ComponentName(getActivity().getApplicationContext(),
HomeScreenActivity.class);
prefEnableDisableHomeScreen = (SwitchPreference) findPreference("enable_disable_home_screen");
prefEnableDisableHomeScreen.setChecked(getIsComponentEnabled(homeScreenComponent));
prefEnableDisableHomeScreen.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
#Override
public boolean onPreferenceChange(Preference preference, Object o) {
boolean previousState = prefEnableDisableHomeScreen.isChecked();
setComponentEnabledSetting(homeScreenComponent, previousState
? PackageManager.COMPONENT_ENABLED_STATE_DISABLED
: PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
prefEnableDisableHomeScreen.setChecked(getIsComponentEnabled(homeScreenComponent));
return false;
}
});
}
private boolean getIsComponentEnabled(ComponentName componentName) {
int state = packageManager.getComponentEnabledSetting(componentName);
return PackageManager.COMPONENT_ENABLED_STATE_ENABLED == state;
}
private void setComponentEnabledSetting(ComponentName componentName, int newState) {
packageManager.setComponentEnabledSetting(componentName, newState, PackageManager.DONT_KILL_APP);
}
Hope this helps.

Android Web Intent Issue

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.

bring Android activity to front from background thread

I have an activity that starts the system browser via Intent. Short before it does that I do a HTTP GET to some other URL. This GET will be answered as soon as the user finishes his task in the browser (logging in using OAuth).
I'd like to be able to close down the browser and / or get my application's activity back to the front.
I do not want to use a WebView because I'd like to avoid the perception that I might be trying to spy on passwords.
Any idea how to solve this? Is it possible at all?
Thanks a bunch!
Daniel
Make sure OAuth opens up URL which is something like yourapp://success
Next you add a intent filter to handle this custom protocol and address. More details are at http://developer.android.com/guide/topics/intents/intents-filters.html#ifs
Here's what did the trick in my project.
The application manifest is pretty standard:
<activity
android:name=".MainActivity"
android:label="#string/app_name"
android:launchMode="singleTask"
android:screenOrientation="portrait" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
Here's a code snippet of the background thread that sends the intent to re-show the activity:
public class AlarmThread extends Thread {
private int mSleepTime;
public AlarmThread(int sleepSeconds) {
super("AlarmThread");
mSleepTime = sleepSeconds * 1000;
}
#Override
public void run() {
Log.i("thread", "started sleeping for " + mSleepTime + " milliseconds");
try {
Thread.sleep(mSleepTime);
} catch (InterruptedException e) {
// ignored
}
Log.i("thread", "creating intent to bring activity to foreground");
Intent intent = new Intent(MainActivity.getContext(), MainActivity.class);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
MainActivity.getContext().getApplicationContext().startActivity(intent);
}
}
Note that the trick is in the MainActivity.getContext().getApplicationContext().startActivity(intent); part (last line above).
In the MainActivity, I added the getContext method:
public static Context getContext() {
return mInstance;
}
And the member 'mInstance' is set in the 'onCreate':
private static MainActivity mInstance = null;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Other code....
mInstance = this;
}

how can first activity be chosen at run time based on data?

i'm just starting out with Android and i think i'm missing something.
It seems like in Android you decide at development time which activity will be the first to be displayed in your application.
i would like to write my application in such a way that some kind of a centralized controller starts executing and it decides which activity should be first
(for example, based on some data obtained from somewhere)
is that possible to do, and if so, how?
thanks.
Most folks do it by launching an activity that just picks up the config it needs and then starts up the "real" activity. One hiccup is that the activity first launched will be on the task stack, but if you set android:noHistory="true" for the initial activity the process should be invisible to the user.
The below method can be used for showing tutorial screens on first app launch.
AndroidManifest.xml:
<activity android:name=".activities.LaunchActivity"
android:noHistory="true"
android:theme="#android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".onboarding.OnboardingActivity"/>
<activity android:name=".activities.MainActivity"/>
LaunchActivity.java:
public class LaunchActivity extends Activity {
public static final String FIRST_APP_LAUNCH = "com.your.package.name";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (isFirstAppLaunch()) {
setFirstAppLaunch(false);
startActivity(new Intent(this, OnboardingActivity.class));
} else {
startActivity(new Intent(this, MainActivity.class));
}
finish();
}
private boolean isFirstAppLaunch() {
SharedPreferences preferences = this.getPreferences(Context.MODE_PRIVATE);
return preferences.getBoolean(FIRST_APP_LAUNCH, true);
}
private void setFirstAppLaunch(boolean value) {
SharedPreferences preferences = this.getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean(FIRST_APP_LAUNCH, value);
editor.apply();
}
}
I wonder What so tough in this. in the main Activity in the onCreate Method after checking the data starting another activity without setting the view content of Main Activity.

Categories

Resources