I need to get the name of the app chosen by user fired with Intent.ACTION_SEND for analytic purposes. The name of app will be obtained through BroadcastReceiver.
It works until one day, the security engineer in our team informed us that all PendingIntent in the codebase must have PendingIntent.FLAG_IMMUTABLE to be secure.
The flag added breaks the existing functionality because intent?.getParcelableExtra<ComponentName>(Intent.EXTRA_CHOSEN_COMPONENT)?.packageName will always return null.
Is there anything I can do? PendingIntent.FLAG_MUTABLE is sadly not an option for me.
You can find same way of doing this from Android Documentation - Getting information about sharing
MainActivity.kt
const val PENDING_INTENT_REQUEST_CODE = 0x1000
const val THIRD_PARTY_SHARE_REQUEST_CODE = 0x1001
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnShare.setOnClickListener {
openThirdPartyShareDialog()
}
}
private fun openThirdPartyShareDialog() {
val thirdPartyShareIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
}
val broadcastIntent = Intent(this, ThirdPartyAppBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(this,
PENDING_INTENT_REQUEST_CODE,
broadcastIntent,
getPendingFlagIntent()
)
startActivityForResult(Intent.createChooser(
thirdPartyShareIntent,
null,
pendingIntent.intentSender
), THIRD_PARTY_SHARE_REQUEST_CODE)
}
private fun getPendingFlagIntent(): Int {
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= 23) {
flags = flags or PendingIntent.FLAG_IMMUTABLE
}
return flags
}
}
ThirdPartyAppBroadcastReceiver.kt
class ThirdPartyAppBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// packageName will always be null !
val packageName =
intent?.getParcelableExtra<ComponentName>(Intent.EXTRA_CHOSEN_COMPONENT)?.packageName
}
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.flamyoad.broadcast_share"
>
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/Theme.Broadcastshare"
>
<receiver android:name="com.flamyoad.broadcast_share.ThirdPartyAppBroadcastReceiver" />
<activity
android:name=".MainActivity"
android:exported="true"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Turns out it's fine to remove PendingIntent.FLAG_IMMUTABLE if you're using explicit intent.
Android apps send messages between components using Intents. Intents
can either specify the target component (Explicit Intent) or list a
general action and let the operating system deliver the Intent to any
component on the device that registers an Intent Filter matching that
action (Implicit Intent).
PendingIntents are Intents delegated to another app to be delivered at
some future time. Creating an implicit intent wrapped under a
PendingIntent is a security vulnerability that might lead to
denial-of-service, private data theft, and privilege escalation.
You can read more about it from Remediation for Implicit PendingIntent Vulnerability
I am writing custom launcher and almost at the end of my work. I would like to have the functionality to add PWA to my launcher home screen.
I've created new activity that will handle new PWAs, added it to AndroidManifest with CONFIRM_PIN_SHORTCUT intent filter and managed to receive PinItemRequest in my activity when pwa got installed from browser.
AndroidManifest.xml
<activity android:name=".HandleShortcutActivity">
<intent-filter>
<action android:name="android.content.pm.action.CONFIRM_PIN_SHORTCUT" />
</intent-filter>
</activity>
HandleShortcutActivity.kt:
class HandleShortcutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
handlePins()
}
//LauncherApps.getPinItemRequest( this.intent );
}
#RequiresApi(Build.VERSION_CODES.O)
private fun handlePins() {
val launcherApps = applicationContext.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
val request = launcherApps.getPinItemRequest(intent)
Timber.d("handlePins: request = \n{shortcutInfo:\n\tlongLabel=${request.shortcutInfo?.longLabel}\n\tpackage=${request.shortcutInfo?.`package`}")
this.finish()
}
}
What information from shortcutInfo I can use to correctly run PWA from launcher? The package of PWA is com.android.chrome and it is accessible, but I can't see any additional info that could help me to run PWA.
Also, how can I get the icon from this received PinItemRequset which will I use as an icon in my launcher?
I have an app in which fingerprint lock is used to protect the app from being used without first authenticating with the fingerprint.
The issue is that the verification only happens at first launch of the app; so after first launch, no more verification until when the app is being launched again,
But I want the current verification to be valid when moving across different activities in the app, but it should be invalidated if the user minimises the app or when the phone screen goes off, so that the user would be asked to verify fingerprint again after minimizing and resuming back into the app.
I have tried using a base activity and overriding the onPause and onResume, but the methods get called even when moving across activities, which is not what I want.
You can likely use the system broadcast intents ACTION_SCREEN_ON, ACTION_SCREEN_OFF, and/or ACTION_USER_PRESENT to achieve your goals. Check https://developer.android.com/reference/android/content/Intent for more on these.
To do so, register as a broadcast receiver to receive these notifications in your main activity onCreate, un-register in onDestroy. You can use these notifications to set flags indicating whether authentication is needed.
Example code:
#Override
protected void onCreate(Bundle savedInstanceState)
{
.
.
.
// register to receive appropriate events
IntentFilter intentUserPresent = new IntentFilter(Intent.ACTION_USER_PRESENT);
intentUserPresent.addAction(Intent.ACTION_USER_PRESENT);
registerReceiver(userActionReceiver, intentUserPresent);
.
.
.
}
// create a BroadcastReceiver to receive the notifications
private BroadcastReceiver userActionReceiver = new BroadcastReceiver()
{
#Override
public void onReceive(Context context, Intent intent)
{
Log.d("MyLog", String.format("MainActivity received broadcaast %s", intent.toString()));
if (intent.getAction() == Intent.ACTION_USER_PRESENT)
{
// set flag indicating re-authentication is needed
}
}
};
#Override
protected void onDestroy()
{
// make sure to un-register the receiver!
unregisterReceiver(userActionReceiver);
super.onDestroy();
}
This won't detect a user "minimizing the app", but its unclear why you would consider that re-authentication would be needed in this scenario. Lifecycle events onStart/onStop can detect background/foreground of an activity.
Solved this few weeks ago while working on a completely different project from the one I was working on when I asked the question.
I am glad to share my workaround.
The app is locked after three seconds of minimizing the app or the screen going off; as long as the fingerprint switch is enabled in the app's preferences setting.
First, I created a BaseActivity class from which all other project activities inherit from except the Fingerprint activity class which inherits directly from AppCompatActivity()
package com.domainName.appName.ui.activities
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
open class BaseActivity : AppCompatActivity() {
private var securitySet=false
private var backPressed=false
private var goingToFingerprint=false
companion object{
private var handler:Handler?=null
}
override fun
onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefSaver.save(prefFileName, "launched_new_activity",sValue = true,saveImmediately=true)
if(handler==null){
handler= Handler(mainLooper)
}
handler?.removeCallbacksAndMessages(null)
}
override fun onDestroy() {
super.onDestroy()
handler?.removeCallbacksAndMessages(null)
}
override fun onStart() {
handler?.removeCallbacksAndMessages(null)
goingToFingerprint=false
securitySet=prefChecker.check(prefFileName,"fingerprint_security_switch",false)
super.onStart()
}
override fun onPause() {
super.onPause()
prefSaver.save(prefFileName,"launched_new_activity",sValue = false,saveImmediately=true)
if(securitySet && !backPressed){
if(!goingToFingerprint){postLock()}
}
}
override fun onResume() {
super.onResume()
backPressed=false
if(prefChecker.check(prefFileName,"app_locked",false)&&securitySet){
prefSaver.save(prefFileName,"launched_new_activity",sValue = true,saveImmediately=true)
goingToFingerprint=true
startActivity(Intent(this,FingerprintActivity::class.java))
}
}
override fun onBackPressed() {
backPressed=true
super.onBackPressed()
}
private fun postLock(){
handler?.removeCallbacksAndMessages(null)
handler?.postDelayed({
if(!prefChecker.check(prefFileName,"launched_new_activity",false)) {
prefSaver.save(prefFileName,"app_locked",sValue = true,saveImmediately=true)
}
},3000)
}
}
I created an InitialActivity which has noHistory mode set in the Android manifest and acts as the app launcher activity.
The InitialActivity inherits from BaseActivity as well and simply calls startActivity() in its onCreate method for the MainActivity activity.
The InitialActivity overrides onPause this way :
override fun onPause() {
if(prefChecker.check(prefFileName,"fingerprint_security_switch",false)) {
prefSaver.save(prefFileName,"app_locked",sValue = true,true)
/*setting this so app is locked on First Launch if the fingerprint switch is on.
The fingerprint switch is turned on and off from the SettingsActivity */
}
super.onPause()
}
Then FingerprintActivity is implemented this way:
package com.domainName.appName.ui.activities
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.Gravity
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.PromptInfo
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
class FingerprintActivity : AppCompatActivity() {
private lateinit var executor: Executor
var biometricPrompt: BiometricPrompt? = null
var promptInfo: PromptInfo? = null
var notifIntentLabel: String? = null
var finishImmediately=false
override fun
onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);setContentView(R.layout.fingerprint_main)
executor = ContextCompat.getMainExecutor(this)
finishImmediately = intent.getBooleanExtra("finishImmediately",false)
if(finishImmediately)finish()
biometricPrompt = BiometricPrompt(this,
executor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
biometricPrompt?.cancelAuthentication()
val intent=Intent(this#FingerprintActivity,FingerprintActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.putExtra("finishImmediately",true)
startActivity(intent)
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
prefSaver.save(prefFileName,"app_locked",sValue = false,true)
finish()
}
})
promptInfo = PromptInfo.Builder()
.setTitle("AppName Biometric Login")
.setSubtitle("\t\tLog in using your fingerprint")
.setNegativeButtonText("Cancel")
.build()
val biometricManager = BiometricManager.from(this)
when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) {
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> showToast("No fingerprint hardware detected on this device.")
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> showToast("Fingerprint hardware features are currently unavailable on this device.")
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> showToast("There is no any fingerprint credential associated with your account.\nPlease go to your phone settings to enrol fingerprints.")
BiometricManager.BIOMETRIC_SUCCESS -> authenticateUser()
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> showToast("Fingerprint feature on device requires updating and is therefore currently unavailable.")
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> showToast("Fingerprint features not supported on device")
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> showToast("Fingerprint features status unknown")
}
}
private fun showToast(txt_to_show: String) {
val t = Toast.makeText(this, txt_to_show, Toast.LENGTH_SHORT)
t.setGravity(Gravity.CENTER or Gravity.CENTER_HORIZONTAL, 0, 0)
t.show()
}
private fun authenticateUser() {
if(!finishImmediately){
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.USE_FINGERPRINT
) == PackageManager.PERMISSION_GRANTED
) {
promptInfo?.let{ biometricPrompt?.authenticate(it)}
} else if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.USE_BIOMETRIC
) == PackageManager.PERMISSION_GRANTED
) {
promptInfo?.let{ biometricPrompt?.authenticate(it)}
} else {
showToast("Please grant the AppName app the necessary permissions to use fingerprint hardware.")
}
}
}
}
prefSaver and prefChecker are basically the class. The names were used for descriptive purpose. The class was implemented this way :
package com.domainName.appName.utils
import android.annotation.SuppressLint
import android.content.Context;
import android.content.SharedPreferences;
class PrefCheckerOrSaver(private val context:Context?){
fun <T>check(sFile: String,keyS: String,defaultS: T):T{
if(context==null)return defaultS
val sPref:SharedPreferences= context.getSharedPreferences(sFile,Context.MODE_PRIVATE)
return when(defaultS){
is String->sPref.getString(keyS,defaultS) as T
is Int-> sPref.getInt(keyS,defaultS) as T
is Long-> sPref.getLong(keyS,defaultS) as T
is Boolean-> sPref.getBoolean(keyS,defaultS) as T
else-> defaultS
}
}
#SuppressLint("ApplySharedPref")
fun save(sFile:String, sKey:String, sValue:Any, saveImmediately:Boolean=false){
if(context==null)return
val sharedPreferences:SharedPreferences = context.getSharedPreferences(sFile, Context.MODE_PRIVATE);
val editor:SharedPreferences.Editor = sharedPreferences.edit();
when(sValue) {
is String-> editor.putString(sKey, sValue)
is Int->editor.putInt(sKey, sValue)
is Long-> editor.putLong(sKey, sValue)
is Boolean->editor.putBoolean(sKey, sValue)
}
if(saveImmediately){editor.commit()}
else{ editor.apply()}
}
}
Permission in app manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
package="com.domainName.appName">
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<application
android:name=".MainApplication"
android:allowBackup="false"
android:icon="${appIcon}"
android:label="#string/app_name"
android:roundIcon="${appIconRound}"
android:resizeableActivity="true"
android:supportsRtl="false"
android:theme="#style/ThemeAppMain"
tools:targetApi="n"
>
<activity
android:name=".ui.activities.InitialActivity"
android:label="#string/app_name"
android:exported="true"
android:noHistory="true"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ui.activities.BaseActivity" />
<activity android:name=".ui.activities.MainActivity" android:launchMode="singleTask"/>
<activity android:name=".ui.activities.SettingsActivity" />
<activity android:name=".ui.activities.FingerprintActivity"
android:launchMode="singleTask"
android:noHistory="true"
/>
</application>
</manifest>
I'm using the action_PROCESS_TEXT intent to provide a custom text selection action. On Marshmallow I am getting my action displayed on both readonly text and EditText controls and my code successfully displays/returns results.
On Oreo (Pixel 2 XL) my action is successfully displayed on read only text but I am not getting my action displayed on EditText controls, either in my own app or others. I also notice that other process text app's such as Google Translate are only randomly displayed.
Google docs are very basic for this and I'm relying on the original blog post, has something changed?
Here's my relevant manifest and activity code
Manifest
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="false"
android:theme="#style/AppTheme">
...
<activity
android:name=".TranslateActivity"
android:label="#string/action_name"
android:theme="#style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
</application>
</manifest>
Activity
package mynamespace
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_translate.*
class TranslateActivity : AppCompatActivity() {
private var readOnly = true
val translator = Translator()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_translate)
setTitle(R.string.app_name)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
toolbar.setNavigationOnClickListener({
onBackPressed()
})
translator.loadTranslations()
handleIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
private fun handleIntent(intent: Intent) {
if (intent.hasExtra(Intent.EXTRA_PROCESS_TEXT_READONLY)) {
readOnly = intent.getBooleanExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false)
}
if (intent.hasExtra(Intent.EXTRA_PROCESS_TEXT)) {
val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT).toString()
val result: String = translator.translate(text.toLowerCase())
if (readOnly) {
// Display result
textViewTranslation.text = result
} else {
replaceText(result)
}
}
}
private fun replaceText(replacementText: String) {
val intent = Intent()
intent.putExtra(Intent.EXTRA_PROCESS_TEXT, replacementText)
setResult(Activity.RESULT_OK, intent)
finish()
}
...
}
ACTION_PROCESS_TEXT does not work very well on Android 8.x.
The good news is that this appears to be fixed in Android P. You won't have any problems in 2025 and onwards, once all the Android O devices get retired.
Maybe stupid question, but I have already spent to many hours on this.
I have my Kotlin listener:
package pl.bmideas.michal.bmnotifier
public class MyNotificationListener : NotificationListenerService() {
private var apiService :BackendApi? = null;
override fun onCreate() {
Log.i("MyNotificationListener" , "Creating NotificationListenerService service")
super.onCreate()
(.........SOMETHING ELSE..............)
}
override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "DESTROING")
(.........SOMETHING ELSE..............)
}
override fun onNotificationRemoved(sbn: StatusBarNotification) {
val sbnInfo = StatusBarNotificationExtended(sbn)
Log.i(TAG, "REMOVED")
}
override fun onNotificationPosted(sbn: StatusBarNotification) {
Log.i(TAG, "RECIVED`")
(.........SOMETHING ELSE..............)
}
companion object {
var TAG = "MyNotificationListener"
}
}
and my config looks looks this:
<service
android:enabled="true"
android:name="pl.bmideas.michal.bmnotifier.MyNotificationListener"
android:label="#string/service_name"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
I'm not doing anything special in Activity.
Yes - I've checked security option and my app has access to notifications.
Yes - I've tried pointing to service by dot instead of full package
In logcat I can only see:
12-23 12:56:54.989 889-889/? V/NotificationListeners: enabling notification listener for 0:
ComponentInfo{pl.bmideas.michal.bmnotifier/pl.bmideas.michal.bmnotifier.MyNotificationListener}
I cant get instance unless i will bidn to this service in Activity wchich creates the service but still I get no info in logcat about notifications.
Can you guys help?
Holly....
after rewriting this code to pure Java it works..... but why?