Android Intents: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
 
(26 intermediate revisions by the same user not shown)
Line 28: Line 28:
intent.type = "text/plain"
intent.type = "text/plain"
startActivity(intent)
startActivity(intent)
</syntaxhighlight>
==Implicit With Choice==
Android looks at the action, and prompts the user for all app which handle this.The user can make their choice a default however we can override this and force a choice. Notice we should always check for a valid intent or the app will crash
<syntaxhighlight lang="kotlin">
val chooser = Intent.createChooser(myIntent, title)
if(intent.resolveActivity(packageManager) !=null) {
  startActivity(chooser)
} else {
  Log.d(...)
}
</syntaxhighlight>
=Common Intents=
==What is Required==
For common intents we need to go to https://developer.android.com/guide/components/intents-common#Clock and look at what is required this includes
*Action Type
*Permissions
*Sample Code
*Pass the appropriate Parameter
==Working Example 1 SET_ALARM==
This creates an alarm for Mon-Fri at 17:00.
*Set Permissions
*Implementation
===Set Permissions===
<syntaxhighlight lang="kotlin">
<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>
</syntaxhighlight>
===Implementation===
<syntaxhighlight lang="kotlin">
        val intent = Intent(AlarmClock.ACTION_SET_ALARM).apply {
            putExtra(AlarmClock.EXTRA_MESSAGE, "My Great Alarm")
            putExtra(AlarmClock.EXTRA_HOUR, 17)
            putExtra(AlarmClock.EXTRA_MINUTES, 0)
            putExtra(
                AlarmClock.EXTRA_DAYS,
                    arrayOf(
                        java.util.Calendar.MONDAY,
                        java.util.Calendar.TUESDAY,
                        java.util.Calendar.WEDNESDAY,
                        java.util.Calendar.THURSDAY,
                        java.util.Calendar.FRIDAY
                    )
                )
            }
        if (intent.resolveActivity(packageManager) != null) {
            startActivity(intent)
        }
</syntaxhighlight>
==Working Example 2 CREATE_NOTE==
It seems that the documentation is a bit poor around the intents. So I thought it wise just to see how it worked for this for me
*Set Permissions
*Implementation
===Set Permissions===
None specified
<syntaxhighlight lang="kotlin">
</syntaxhighlight>
===Implementation===
This is the documentation at the time
<syntaxhighlight lang="kotlin">
    val intent = Intent(NoteIntents.ACTION_CREATE_NOTE).apply {
        putExtra(NoteIntents.EXTRA_NAME, "test subject")
        putExtra(NoteIntents.EXTRA_TEXT, "text")
    }
    if (intent.resolveActivity(packageManager) != null) {
        startActivity(intent)
    }
</syntaxhighlight>
===Fixing===
This failed to work so rather than following the instructor I googled my way so I might reuse this approach next time
====Initial====
I google the documentation for NoteIntents and arrived on https://developers.google.com/android/reference/com/google/android/gms/actions/NoteIntents. There is no mention of a library required
<br>
Next Stack at https://stackoverflow.com/questions/50145470/how-do-i-use-noteintents shows this did show play services was required and then a light bulb moment, I pressed F12 lead me to the NoteIntents.class which gives
<syntaxhighlight lang="java">
public class NoteIntents {
    @RecentlyNonNull
    public static final String ACTION_CREATE_NOTE = "com.google.android.gms.actions.CREATE_NOTE";
...
</syntaxhighlight>
====Packager====
So with this install the error was now that the packer is null
<syntaxhighlight lang="kotlin">
if(intent.resolveActivity(packageManager) !=null)
</syntaxhighlight>
<br>
This led me to move off my emulator, genymotion which does not use playservices. So I installed play services on the android emulator and install '''Keep Notes''' because this is said to work with Noteintents. I had another go with no joy. I read that SDK 30 means you need to do something different https://developer.android.com/about/versions/11/privacy/package-visibility so I lowered the emulator to 29 and install play services. But no joy.
====Light Bulb #2====
Well I read above
<syntaxhighlight lang="kotlin">
val chooser = Intent.createChooser(myIntent, title)
</syntaxhighlight>
So I implemented a chooser and still no prompt using
<syntaxhighlight lang="kotlin">
        val intent = Intent(NoteIntents.ACTION_CREATE_NOTE).apply {
            putExtra(NoteIntents.EXTRA_NAME, "My Subject")
            putExtra(NoteIntents.EXTRA_TEXT, "Iain you found it")
            type = "text/plain"
        }
</syntaxhighlight>
So I removed the type and finally it said '''no apps can perform this action'''. So adding the type back in showed nothing.
====Resolution====
To me this said that there are no clients which supports "text/plain" so I googled how make a client and basically implemented an app with
<syntaxhighlight lang="xml">
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/Theme.TestNoteIntent.NoActionBar" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="com.google.android.gms.actions.CREATE_NOTE" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="*/*" />
            </intent-filter>
        </activity>
</syntaxhighlight>
And in the Activity
<syntaxhighlight lang="kotlin">
        when {
            intent?.action == NoteIntents.ACTION_CREATE_NOTE -> {
                if ("text/plain" == intent.type) {
                    intent.getStringExtra(NoteIntents.EXTRA_TEXT)?.let {
                        Toast.makeText(this,it,
                                Toast.LENGTH_LONG).show();
                    }
                }
                else {
                    Toast.makeText(this, "We go this ok 1!",
                            Toast.LENGTH_LONG).show();
                }
...
</syntaxhighlight>
This finally produced a prompt for '''both''' my Test app and Keep Notes. Using the test app all went well. I could not get Keep Notes to work
==Working Example 3 ACTION_VIEW==
Thought I would add a geo example just in case.
<syntaxhighlight lang="kotlin">
        val address = "254 Badger Avenue, Crewe"
        val intent = Intent(Intent.ACTION_VIEW).apply {
            data = Uri.parse("geo:0,0?q=$address")
        }
        if (intent.resolveActivity(packageManager) != null) {
            startActivity(intent)
        }
</syntaxhighlight>
==Working Example 4 ACTION_PICK==
===Introduction====
So thought I might look at getting contacts. This looked quite simple but of course it is not. Android seems to move quickly with its api. Firstly,
====Add Permissions====
We need to add this in the example I did.
<syntaxhighlight lang="xml">
<uses-permission android:name="android.permission.READ_CONTACTS" />
</syntaxhighlight>
====Call The Intent====
The documentation pointed to doing the following. But this is depreciated
<syntaxhighlight lang="kotlin">
    val intent = Intent(Intent.ACTION_PICK).apply {
        type = ContactsContract.Contacts.CONTENT_TYPE
    }
    if (intent.resolveActivity(packageManager) != null) {
        startActivityForResult(intent, REQUEST_SELECT_CONTACT)
    }
</syntaxhighlight>
<br>
Currently ha ha we need to now do,
<syntaxhighlight lang="kotlin">
        val intent = Intent(Intent.ACTION_PICK).apply {
            type = ContactsContract.Contacts.CONTENT_TYPE
        }
        if (intent.resolveActivity(packageManager) != null) {
            someActivityResultLauncher.launch(intent);
        }
</syntaxhighlight>
Where someActivityResultLauncher is a declarative function
<syntaxhighlight lang="kotlin">
    var someActivityResultLauncher = registerForActivityResult(
        StartActivityForResult()
    ) { result ->
        if (result.resultCode == Activity.RESULT_OK) {
            val uri: Uri = result?.data?.data ?: return@registerForActivityResult
            getDetails(uri)
        }
    }
</syntaxhighlight>
====Permissions====
I received Permission issues with this so added the standard
<syntaxhighlight lang="kotlin">
    fun isReadContactsPermissionGranted(): Boolean {
        return if (Build.VERSION.SDK_INT >= 23) {
            if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
                == PackageManager.PERMISSION_GRANTED
            ) {
                Log.v(TAG, "Permission is granted")
                true
            } else {
                Log.v(TAG, "Permission is revoked")
                ActivityCompat.requestPermissions(
                    this,
                    arrayOf(Manifest.permission.READ_CONTACTS),
                    1
                )
                false
            }
        } else { //permission is automatically granted on sdk<23 upon installation
            Log.v(TAG, "Permission is granted")
            true
        }
    }
</syntaxhighlight>
====Getting the Contact====
I was using an emulator with my own contacts. Using the API showed that the call to contentResolver.query failed because of the column ContactsContract.CommonDataKinds.Phone.NUMBER. Digging into this, not all columns are available and you can query if there is a phone number. This is part of that code.
<syntaxhighlight lang="kotlin">
    private fun getDetails(uri: Uri) {
        val cr = contentResolver
        val cur = cr.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null)
        val projection = arrayOf(
            ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
            ContactsContract.CommonDataKinds.Phone.HAS_PHONE_NUMBER,
//            ContactsContract.CommonDataKinds.Phone.NUMBER,
//            ContactsContract.CommonDataKinds.Email.DATA
        )
        val names = contentResolver.query(uri, projection, null, null, null)
        val indexName = names!!.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
        val indexHasPhoneNumber = names!!.getColumnIndex(ContactsContract.CommonDataKinds.Phone.HAS_PHONE_NUMBER)
        // val indexNumber = names.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
        names.moveToFirst()
        do {
            val name = names.getString(indexName)
            Log.e("Name new:", name)
            val hasPhoneNumberAsInt = names.getInt(indexHasPhoneNumber)
            val hasPhoneNumber = hasPhoneNumberAsInt.toString().toBoolean()
            Log.e("Has Phone Number:", hasPhoneNumber.toString())
            // val number = names.getString(indexNumber)
            // Log.e("Number new:", "::$number")
        } while (names.moveToNext())
</syntaxhighlight>
You can query the columns available on the cursor with
<syntaxhighlight lang="kotlin">
        if(cur !=null)
        {
            var temp = cur.getColumnNames()
        }
</syntaxhighlight>
==Sending Emails==
Finally some sanity with Common Intents. We can send emails with
*ACTION_SENDTO, one email no attachments
*ACTION_SEND, one attachment
*ACTION_SEND_MULTIPLE, multiple attachments
To send to multiple people just
<syntaxhighlight lang="kotlin">
        putExtra(Intent.EXTRA_EMAIL,arrayOf(
            "someone@gmail1.com",
            "someone@gmail2.com")
</syntaxhighlight>
==Capturing Images==
To do this we need to
*Get a Uri
*Call ACTION_IMAGE_CAPTURE with Uri
*Using the Result
===Get Uri===
Searching the web I found this example.
<syntaxhighlight lang="kotlin">
    private fun getImageFileUri(): Uri? {
        // Create a storage directory for the images
        // To be safe(er), you should check that the SDCard is mounted
        // using Environment.getExternalStorageState() before doing this
        val imagePath = File(
            Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_PICTURES
            ), "Tuxuri"
        )
        Log.d(TAG, "Find " + imagePath.getAbsolutePath())
        if (!imagePath.exists()) {
            if (!imagePath.mkdirs()) {
                Log.d("CameraTestIntent", "failed to create directory")
                return null
            } else {
                Log.d(TAG, "create new Tux folder")
            }
        }
        // Create an image file name
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val image = File(imagePath, "TUX_$timeStamp.jpg")
        if (!image.exists()) {
            try {
                image.createNewFile()
            } catch (e: IOException) {
                // TODO Auto-generated catch block
                e.printStackTrace()
            }
        }
        //return image;
        // Create an File Uri
        return Uri.fromFile(image)
    }
</syntaxhighlight>
===Call ACTION_IMAGE_CAPTURE with Uri===
Next we call the action
<syntaxhighlight lang="kotlin">
        mUriSavedImage = getImageFileUri()
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
            putExtra(MediaStore.EXTRA_OUTPUT, mUriSavedImage)
        }
        if (intent.resolveActivity(packageManager) != null) {
            attachmentActivityResultLauncher.launch(intent);
        }        }
</syntaxhighlight>
===Using the Result===
Like the contacts where we retrieve a result we need to declare a handler. I do not recommend putting the code in the handler so this is just for show
<syntaxhighlight lang="kotlin">
    var attachmentActivityResultLauncher = registerForActivityResult(
        StartActivityForResult()
    ) { result ->
        if (result.resultCode == Activity.RESULT_OK ) {
            val bitmap = MediaStore.Images.Media.getBitmap(contentResolver,mUriSavedImage)
            imageViewAttachmentPreview.visibility = View.VISIBLE
            imageViewAttachmentPreview.setImageBitmap(bitmap)
        }
    }
</syntaxhighlight>
===And Finally Esther===
This was done under SDK 29 however when I moved to SDK 30 it failed.
<br>
====Attempt 1 Use a FileProvider====
A first I thought this was how I created the Uri. A first approach was to create a file as before but to use a FileProvider. So to create the file. This did work at 29 and is here for reference. However it was not the solution.
<syntaxhighlight lang="kotlin">
    @SuppressLint("SimpleDateFormat")
    private fun createImageFile(): File {
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        val file = File.createTempFile(
            "TUX_${timeStamp}_",
            ".jpg",
            storageDir)
        return file.absoluteFile
    }
</syntaxhighlight>
<br>
And then to create the Uri
<syntaxhighlight lang="kotlin">
        mUriSavedImage = FileProvider.getUriForFile(
            this,
            BuildConfig.APPLICATION_ID + ".provider", createImageFile())
</syntaxhighlight>
To support this you must add it the provide to the manifest
<syntaxhighlight lang="xml">
        <provider
                android:name="androidx.core.content.FileProvider"
                android:authorities="${applicationId}.provider"
                android:exported="false"
                android:grantUriPermissions="true">
            <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/provider_paths">
            </meta-data>
        </provider>
</syntaxhighlight>
And a provide path in res/xml
<syntaxhighlight lang="xml">
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external_files" path="."/>
</paths>
</syntaxhighlight>
====Attempt 2 Specify the ACTION in the Manifest====
Eventually the problem was resolved by adding the action to the queries/intent section of the manifest.
<syntaxhighlight lang="xml">
    <queries>
        <intent>
            <action android:name="android.media.action.IMAGE_CAPTURE" />
        </intent>
    </queries>
</syntaxhighlight>
=Revisit Intents 2025=
==Introduction==
Just another look at these from last time I messed with android. A few changes with compose but nothing to amazing
==Explicit Intent when Launching YouTube==
We can find the package name using adb
<syntaxhighlight lang="bash">
adb shell pm list packages |grep -i youtube
</syntaxhighlight>
Now we can launch by adding the query to the manifest
<syntaxhighlight lang="xml">
    <queries>
        <package android:name="com.google.android.youtube" />
    </queries>
</syntaxhighlight>
And Launch with this code
<syntaxhighlight lang="kotlin">
Button(
  onClick = {
    val intent: Intent? = context.packageManager.getLaunchIntentForPackage("com.google.android.youtube")
    if (intent != null) context.startActivity(intent)
  },
  modifier = Modifier
    .fillMaxWidth()
    .padding(8.dp)
) {
  Text("Launch YouTube")
}
</syntaxhighlight>
==Implicit Intent when an ACTION==
In our case we are using the ACTION_SEND which maybe be gmail or some other application. In here we need to specify the action and start the activity. I did not get a warning about not specifying the queries but in the youtube I watched I did. So this is what they did
<syntaxhighlight lang="xml">
    <queries>
        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="text/plain" />
        </intent>
    </queries>
</syntaxhighlight>
And the code to launch it
<syntaxhighlight lang="kotlin">
onClick = {
  val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_EMAIL, arrayOf("iwiseman@bibble.co.nz"))
    putExtra(Intent.EXTRA_SUBJECT, "Hello")
    putExtra(Intent.EXTRA_TEXT, "Hello, I am writing to you to say hello.")
  }
  if (intent.resolveActivity(context.packageManager) != null) {
    context.startActivity(intent)
  }
},
</syntaxhighlight>
==Binding You App from another Activity==
If we want to have our app available we need to
*Specify in Manifest
*Add Code to Retrieve Intent Data
===Specify in Manifest===
We specify this is one instance of an app when launched by changing the launch mode of the activity to singleTop
<syntaxhighlight lang="xml">
<activity
  android:name=".MainActivity"
  android:exported="true"
  android:launchMode="singleTop"
...
</syntaxhighlight>
We specify the filters which we allow to bind to.
<syntaxhighlight lang="xml">
<intent-filter>
  <action android:name="android.intent.action.SEND" />
  <category android:name="android.intent.category.DEFAULT" />
  <data android:mimeType="image/*" />
</intent-filter>
</syntaxhighlight>
===Add Code to Retrieve Intent Data===
Now we need to handle the incoming data with
<syntaxhighlight lang="kotlin">
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        val uri = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
        } else {
            intent.getParcelableExtra(Intent.EXTRA_STREAM)
        }
        viewModel.update(uri)
    }
</syntaxhighlight>
</syntaxhighlight>

Latest revision as of 01:23, 18 March 2025

Introduction

Intents

There are two types of intents

  • Explicit
  • Implicit

Explicit

We can start an explicit intent with

val intent = Intent(this.MyActivityClass::class.java_
startActivity(intent)

Implicit

No destination intent is defined. The user will be prompted for which application to use. Not the use of the apply operator.

val intent = Intent().apply {
    action = Intent.ACTION_SEND
    putExtra(Intent.EXTRA_TEXT,"Hello World")
    type = "text/plain"
}
startActivity(intent)


Quite nice compared with the code without the apply.

val intent = Intent()
intent.action = Intent.ACTION_SEND
intent.putExtra(Intent.EXTRA_TEXT,"Hello World")
intent.type = "text/plain"
startActivity(intent)

Implicit With Choice

Android looks at the action, and prompts the user for all app which handle this.The user can make their choice a default however we can override this and force a choice. Notice we should always check for a valid intent or the app will crash

val chooser = Intent.createChooser(myIntent, title)
if(intent.resolveActivity(packageManager) !=null) {
  startActivity(chooser)
} else {
  Log.d(...)
}

Common Intents

What is Required

For common intents we need to go to https://developer.android.com/guide/components/intents-common#Clock and look at what is required this includes

  • Action Type
  • Permissions
  • Sample Code
  • Pass the appropriate Parameter

Working Example 1 SET_ALARM

This creates an alarm for Mon-Fri at 17:00.

  • Set Permissions
  • Implementation

Set Permissions

<uses-permission android:name="com.android.alarm.permission.SET_ALARM"/>

Implementation

        val intent = Intent(AlarmClock.ACTION_SET_ALARM).apply {
            putExtra(AlarmClock.EXTRA_MESSAGE, "My Great Alarm")
            putExtra(AlarmClock.EXTRA_HOUR, 17)
            putExtra(AlarmClock.EXTRA_MINUTES, 0)
            putExtra(
                AlarmClock.EXTRA_DAYS,
                    arrayOf(
                        java.util.Calendar.MONDAY,
                        java.util.Calendar.TUESDAY,
                        java.util.Calendar.WEDNESDAY,
                        java.util.Calendar.THURSDAY,
                        java.util.Calendar.FRIDAY
                    )
                )
            }

        if (intent.resolveActivity(packageManager) != null) {
            startActivity(intent)
        }

Working Example 2 CREATE_NOTE

It seems that the documentation is a bit poor around the intents. So I thought it wise just to see how it worked for this for me

  • Set Permissions
  • Implementation

Set Permissions

None specified

Implementation

This is the documentation at the time

    val intent = Intent(NoteIntents.ACTION_CREATE_NOTE).apply {
        putExtra(NoteIntents.EXTRA_NAME, "test subject")
        putExtra(NoteIntents.EXTRA_TEXT, "text")
    }
    if (intent.resolveActivity(packageManager) != null) {
        startActivity(intent)
    }

Fixing

This failed to work so rather than following the instructor I googled my way so I might reuse this approach next time

Initial

I google the documentation for NoteIntents and arrived on https://developers.google.com/android/reference/com/google/android/gms/actions/NoteIntents. There is no mention of a library required
Next Stack at https://stackoverflow.com/questions/50145470/how-do-i-use-noteintents shows this did show play services was required and then a light bulb moment, I pressed F12 lead me to the NoteIntents.class which gives

public class NoteIntents {
    @RecentlyNonNull
    public static final String ACTION_CREATE_NOTE = "com.google.android.gms.actions.CREATE_NOTE";
...

Packager

So with this install the error was now that the packer is null

if(intent.resolveActivity(packageManager) !=null)


This led me to move off my emulator, genymotion which does not use playservices. So I installed play services on the android emulator and install Keep Notes because this is said to work with Noteintents. I had another go with no joy. I read that SDK 30 means you need to do something different https://developer.android.com/about/versions/11/privacy/package-visibility so I lowered the emulator to 29 and install play services. But no joy.

Light Bulb #2

Well I read above

val chooser = Intent.createChooser(myIntent, title)

So I implemented a chooser and still no prompt using

        val intent = Intent(NoteIntents.ACTION_CREATE_NOTE).apply {
            putExtra(NoteIntents.EXTRA_NAME, "My Subject")
            putExtra(NoteIntents.EXTRA_TEXT, "Iain you found it")
            type = "text/plain"
        }

So I removed the type and finally it said no apps can perform this action. So adding the type back in showed nothing.

Resolution

To me this said that there are no clients which supports "text/plain" so I googled how make a client and basically implemented an app with

        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/Theme.TestNoteIntent.NoActionBar" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="com.google.android.gms.actions.CREATE_NOTE" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="*/*" />
            </intent-filter>
        </activity>

And in the Activity

        when {
            intent?.action == NoteIntents.ACTION_CREATE_NOTE -> {
                if ("text/plain" == intent.type) {
                    intent.getStringExtra(NoteIntents.EXTRA_TEXT)?.let {
                        Toast.makeText(this,it,
                                Toast.LENGTH_LONG).show();
                    }
                }
                else {
                    Toast.makeText(this, "We go this ok 1!",
                            Toast.LENGTH_LONG).show();
                }
...

This finally produced a prompt for both my Test app and Keep Notes. Using the test app all went well. I could not get Keep Notes to work

Working Example 3 ACTION_VIEW

Thought I would add a geo example just in case.

        val address = "254 Badger Avenue, Crewe"
        val intent = Intent(Intent.ACTION_VIEW).apply {
            data = Uri.parse("geo:0,0?q=$address")
        }
        if (intent.resolveActivity(packageManager) != null) {
            startActivity(intent)
        }

Working Example 4 ACTION_PICK

Introduction=

So thought I might look at getting contacts. This looked quite simple but of course it is not. Android seems to move quickly with its api. Firstly,

Add Permissions

We need to add this in the example I did.

<uses-permission android:name="android.permission.READ_CONTACTS" />

Call The Intent

The documentation pointed to doing the following. But this is depreciated

    val intent = Intent(Intent.ACTION_PICK).apply {
        type = ContactsContract.Contacts.CONTENT_TYPE
    }
    if (intent.resolveActivity(packageManager) != null) {
        startActivityForResult(intent, REQUEST_SELECT_CONTACT)
    }


Currently ha ha we need to now do,

        val intent = Intent(Intent.ACTION_PICK).apply {
            type = ContactsContract.Contacts.CONTENT_TYPE
        }
        if (intent.resolveActivity(packageManager) != null) {
            someActivityResultLauncher.launch(intent);
        }

Where someActivityResultLauncher is a declarative function

    var someActivityResultLauncher = registerForActivityResult(
        StartActivityForResult()
    ) { result ->

        if (result.resultCode == Activity.RESULT_OK) {
            val uri: Uri = result?.data?.data ?: return@registerForActivityResult
            getDetails(uri)
        }
    }

Permissions

I received Permission issues with this so added the standard

    fun isReadContactsPermissionGranted(): Boolean {

        return if (Build.VERSION.SDK_INT >= 23) {

            if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
                == PackageManager.PERMISSION_GRANTED
            ) {
                Log.v(TAG, "Permission is granted")
                true
            } else {
                Log.v(TAG, "Permission is revoked")
                ActivityCompat.requestPermissions(
                    this,
                    arrayOf(Manifest.permission.READ_CONTACTS),
                    1
                )
                false
            }
        } else { //permission is automatically granted on sdk<23 upon installation
            Log.v(TAG, "Permission is granted")
            true
        }
    }

Getting the Contact

I was using an emulator with my own contacts. Using the API showed that the call to contentResolver.query failed because of the column ContactsContract.CommonDataKinds.Phone.NUMBER. Digging into this, not all columns are available and you can query if there is a phone number. This is part of that code.

    private fun getDetails(uri: Uri) {

        val cr = contentResolver
        val cur = cr.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null)

        val projection = arrayOf(
            ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
            ContactsContract.CommonDataKinds.Phone.HAS_PHONE_NUMBER,
//            ContactsContract.CommonDataKinds.Phone.NUMBER,
//            ContactsContract.CommonDataKinds.Email.DATA
        )

        val names = contentResolver.query(uri, projection, null, null, null)
        val indexName = names!!.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
        val indexHasPhoneNumber = names!!.getColumnIndex(ContactsContract.CommonDataKinds.Phone.HAS_PHONE_NUMBER)
        // val indexNumber = names.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)

        names.moveToFirst()
        do {
            val name = names.getString(indexName)
            Log.e("Name new:", name)
            val hasPhoneNumberAsInt = names.getInt(indexHasPhoneNumber)
            val hasPhoneNumber = hasPhoneNumberAsInt.toString().toBoolean()
            Log.e("Has Phone Number:", hasPhoneNumber.toString())
            // val number = names.getString(indexNumber)
            // Log.e("Number new:", "::$number")
        } while (names.moveToNext())

You can query the columns available on the cursor with

        if(cur !=null)
        {
            var temp = cur.getColumnNames()
        }

Sending Emails

Finally some sanity with Common Intents. We can send emails with

  • ACTION_SENDTO, one email no attachments
  • ACTION_SEND, one attachment
  • ACTION_SEND_MULTIPLE, multiple attachments

To send to multiple people just

        putExtra(Intent.EXTRA_EMAIL,arrayOf(
            "someone@gmail1.com",
            "someone@gmail2.com")

Capturing Images

To do this we need to

  • Get a Uri
  • Call ACTION_IMAGE_CAPTURE with Uri
  • Using the Result

Get Uri

Searching the web I found this example.

    private fun getImageFileUri(): Uri? {

        // Create a storage directory for the images
        // To be safe(er), you should check that the SDCard is mounted
        // using Environment.getExternalStorageState() before doing this
        val imagePath = File(
            Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_PICTURES
            ), "Tuxuri"
        )

        Log.d(TAG, "Find " + imagePath.getAbsolutePath())
        if (!imagePath.exists()) {
            if (!imagePath.mkdirs()) {
                Log.d("CameraTestIntent", "failed to create directory")
                return null
            } else {
                Log.d(TAG, "create new Tux folder")
            }
        }

        // Create an image file name
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val image = File(imagePath, "TUX_$timeStamp.jpg")
        if (!image.exists()) {
            try {
                image.createNewFile()
            } catch (e: IOException) {
                // TODO Auto-generated catch block
                e.printStackTrace()
            }
        }

        //return image;

        // Create an File Uri
        return Uri.fromFile(image)
    }

Call ACTION_IMAGE_CAPTURE with Uri

Next we call the action

        mUriSavedImage = getImageFileUri()

        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
            putExtra(MediaStore.EXTRA_OUTPUT, mUriSavedImage)
        }

        if (intent.resolveActivity(packageManager) != null) {
            attachmentActivityResultLauncher.launch(intent);
        }        }

Using the Result

Like the contacts where we retrieve a result we need to declare a handler. I do not recommend putting the code in the handler so this is just for show

    var attachmentActivityResultLauncher = registerForActivityResult(
        StartActivityForResult()
    ) { result ->

        if (result.resultCode == Activity.RESULT_OK ) {
            val bitmap = MediaStore.Images.Media.getBitmap(contentResolver,mUriSavedImage)

            imageViewAttachmentPreview.visibility = View.VISIBLE
            imageViewAttachmentPreview.setImageBitmap(bitmap)
        }
    }

And Finally Esther

This was done under SDK 29 however when I moved to SDK 30 it failed.

Attempt 1 Use a FileProvider

A first I thought this was how I created the Uri. A first approach was to create a file as before but to use a FileProvider. So to create the file. This did work at 29 and is here for reference. However it was not the solution.

    @SuppressLint("SimpleDateFormat")
    private fun createImageFile(): File {
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)

        val file = File.createTempFile(
            "TUX_${timeStamp}_",
            ".jpg",
            storageDir)

        return file.absoluteFile
    }


And then to create the Uri

        mUriSavedImage = FileProvider.getUriForFile(
            this,
            BuildConfig.APPLICATION_ID + ".provider", createImageFile())

To support this you must add it the provide to the manifest

        <provider
                android:name="androidx.core.content.FileProvider"
                android:authorities="${applicationId}.provider"
                android:exported="false"
                android:grantUriPermissions="true">
            <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/provider_paths">
            </meta-data>
        </provider>

And a provide path in res/xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external_files" path="."/>
</paths>

Attempt 2 Specify the ACTION in the Manifest

Eventually the problem was resolved by adding the action to the queries/intent section of the manifest.

    <queries>
        <intent>
            <action android:name="android.media.action.IMAGE_CAPTURE" />
        </intent>
    </queries>

Revisit Intents 2025

Introduction

Just another look at these from last time I messed with android. A few changes with compose but nothing to amazing

Explicit Intent when Launching YouTube

We can find the package name using adb

adb shell pm list packages |grep -i youtube

Now we can launch by adding the query to the manifest

    <queries>
        <package android:name="com.google.android.youtube" />
    </queries>

And Launch with this code

Button(
  onClick = {
    val intent: Intent? = context.packageManager.getLaunchIntentForPackage("com.google.android.youtube") 
    if (intent != null) context.startActivity(intent)
  },
  modifier = Modifier
    .fillMaxWidth()
    .padding(8.dp)
) {
  Text("Launch YouTube")
}

Implicit Intent when an ACTION

In our case we are using the ACTION_SEND which maybe be gmail or some other application. In here we need to specify the action and start the activity. I did not get a warning about not specifying the queries but in the youtube I watched I did. So this is what they did

    <queries>
        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="text/plain" />
        </intent>
    </queries>

And the code to launch it

onClick = {
  val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_EMAIL, arrayOf("iwiseman@bibble.co.nz"))
    putExtra(Intent.EXTRA_SUBJECT, "Hello")
    putExtra(Intent.EXTRA_TEXT, "Hello, I am writing to you to say hello.")
  }

  if (intent.resolveActivity(context.packageManager) != null) {
    context.startActivity(intent)
  }
},

Binding You App from another Activity

If we want to have our app available we need to

  • Specify in Manifest
  • Add Code to Retrieve Intent Data

Specify in Manifest

We specify this is one instance of an app when launched by changing the launch mode of the activity to singleTop

<activity 
   android:name=".MainActivity"
   android:exported="true"
   android:launchMode="singleTop"
...

We specify the filters which we allow to bind to.

<intent-filter>
  <action android:name="android.intent.action.SEND" />
  <category android:name="android.intent.category.DEFAULT" />
  <data android:mimeType="image/*" />
</intent-filter>

Add Code to Retrieve Intent Data

Now we need to handle the incoming data with

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        val uri = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
        } else {
            intent.getParcelableExtra(Intent.EXTRA_STREAM)
        }
        viewModel.update(uri)
    }