Using Implicit Intents

An Intent is an object that describes to the OS something that you want it to do. With the explicit intents that you have created thus far, you explicitly name the activity that you want the OS to start, like:

    val intent = Intent(this, CheatActivity::class.java)
    startActivity(intent)

With an implicit intent, you describe to the OS the job that you want done. The OS then starts the activity that has advertised itself as capable of doing that job. If the OS finds more than one capable activity, then the user is offered a choice.

Parts of an implicit intent

Here are the critical parts of an intent that you can use to define the job you want done:

the action that you are trying to perform

Actions are typically constants from the Intent class. For example, if you want to view a URL, you can use Intent.ACTION_VIEW for your action. To send something, you use Intent.ACTION_SEND.

the location of any data

The data can be something outside the device, like the URL of a web page, but it can also be a URI to a file or a content URI pointing to a record in a ContentProvider.

the type of data that the action is for

This is a MIME type, like text/html or audio/mpeg3. If an intent includes a location for data, then the type can usually be inferred from that data.

optional categories

If the action is used to describe what to do, the category usually describes where, when, or how you are trying to use an activity. Android uses the category android.intent.category.LAUNCHER to indicate that an activity should be displayed in the top-level app launcher. The android.intent.category.INFO category, on the other hand, indicates an activity that shows information about a package to the user but should not show up in the launcher.

So, for example, a simple implicit intent for viewing a website would include an action of Intent.ACTION_VIEW and a data Uri that is the URL of a website.

Based on this information, the OS will launch the appropriate activity of an appropriate application. (Or, if it finds more than one candidate, present the user with a choice.)

An activity advertises itself as an appropriate activity for ACTION_VIEW via an intent filter in the manifest. If you wanted to write a browser app, for instance, you would include the following intent filter in the declaration of the activity that should respond to ACTION_VIEW:

    <activity
        android:name=".BrowserActivity"
        android:label="@string/app_name"
        android:exported="true" >
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:scheme="http" android:host="www.bignerdranch.com" />
        </intent-filter>
    </activity>

To respond to implicit intents, an activity must have the android:exported attribute set to true and, in an intent filter, the DEFAULT category explicitly included. The action element in the intent filter tells the OS that the activity is capable of performing the job, and the DEFAULT category tells the OS that this activity should be considered for the job when the OS is asking for volunteers. This DEFAULT category is implicitly added to every implicit intent.

Implicit intents can also include extras, just like explicit intents. But any extras on an implicit intent are not used by the OS to find an appropriate activity. The action and data parts of an intent can also be used in conjunction with an explicit intent. That would be the equivalent of telling a particular activity to do something specific.

Sending a crime report

Let’s see how this works by creating an implicit intent to send a crime report in CriminalIntent. The job you want done is sending plain text; the crime report is a string. So the implicit intent’s action will be ACTION_SEND. It will not point to any data or have any categories, but it will specify a type of text/plain.

In CrimeDetailFragment’s updateUi() method, set a listener on your new crime report button. Within the listener’s implementation, create an implicit intent and pass it into startActivity(Intent).

Listing 16.8  Sending a crime report (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    private fun updateUi(crime: Crime) {
      binding.apply {
          ...
          crimeSolved.isChecked = crime.isSolved

          crimeReport.setOnClickListener {
              val reportIntent = Intent(Intent.ACTION_SEND).apply {
                  type = "text/plain"
                  putExtra(Intent.EXTRA_TEXT, getCrimeReport(crime))
                  putExtra(
                      Intent.EXTRA_SUBJECT,
                      getString(R.string.crime_report_subject)
                  )
              }

              startActivity(reportIntent)
          }
      }
  }
    ...
}

Here you use the Intent constructor that accepts a string that is a constant defining the action. There are other constructors that you can use depending on what kind of implicit intent you need to create. You can find them all on the Intent reference page in the documentation. There is no constructor that accepts a type, so you set it explicitly.

You include the text of the report and the string for the subject of the report as extras. Note that these extras use constants defined in the Intent class. Any activity responding to this intent will know these constants and what to do with the associated values.

Starting an activity from a fragment works nearly the same as starting an activity from another activity. You call Fragment’s startActivity(Intent) function, which calls the corresponding Activity function behind the scenes.

Run CriminalIntent and press the SEND CRIME REPORT button. Because this intent will likely match many activities on the device, you will probably see a list of activities presented in a chooser (Figure 16.2). You may need to scroll down in the list to see all of the activities.

Figure 16.2  Activities volunteering to send your crime report

Activities volunteering to send your crime report

If you are offered a choice, make a selection. You will see your crime report loaded into the app that you chose. All you have to do is address and send it.

Apps like Gmail and Google Drive require you to log in with a Google account. It is simpler to choose the Messages app, which does not require you to log in. Press New message in the Select conversation dialog window, type any phone number in the To field, and press the Send to phone number label that appears (Figure 16.3). You will see the crime report in the body of the message.

Figure 16.3  Sending a crime report with the Messages app

Sending a crime report with the Messages app

If, on the other hand, you do not see a chooser, that means one of two things. Either you have already set a default app for an identical implicit intent, or your device has only a single activity that can respond to this intent.

Often, it is best to go with the user’s default app for an action. But in this situation, that is not ideal. It is very common for people to use different messaging apps for different groups of people. The user might use WhatsApp with their family, Slack with their coworkers, and Discord with their friends. Here, it would be best to present the user with all of their options for sending a message so they can choose which app to use each time.

With a little extra configuration, you can create a chooser to be shown every time an implicit intent is used to start an activity. After you create your implicit intent as before, you call the Intent.createChooser(Intent, String) function and pass in the implicit intent and a string for the chooser’s title.

Then you pass the intent returned from createChooser(…) into startActivity(…).

In CrimeDetailFragment.kt, create a chooser to display the activities that respond to your implicit intent.

Listing 16.9  Using a chooser (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    private fun updateUi(crime: Crime) {
        binding.apply {
            ...
            crimeReport.setOnClickListener {
                val reportIntent = Intent(Intent.ACTION_SEND).apply {
                    type = "text/plain"
                    putExtra(Intent.EXTRA_TEXT, getCrimeReport(crime))
                    putExtra(
                        Intent.EXTRA_SUBJECT,
                        getString(R.string.crime_report_subject)
                    )
                }

                startActivity(reportIntent)
                val chooserIntent = Intent.createChooser(
                    reportIntent,
                    getString(R.string.send_report)
                )
                startActivity(chooserIntent)
            }
        }
    }
    ...
}

Run CriminalIntent again and press the SEND CRIME REPORT button. As long as you have more than one activity that can handle your intent, you will be offered a list to choose from (Figure 16.4).

This chooser has changed many times over the various versions of Android. On older versions of Android, you might see the title you passed in when creating the chooserIntent on the chooser. On newer versions of Android, you might be presented with the people in your contacts for various apps to select.

Figure 16.4  Sending text with a chooser

Sending text with a chooser

Asking Android for a contact

Now you are going to create another implicit intent that enables users to choose a suspect from their contacts. You could set up the Intent by hand, but it is easier to use the Activity Results APIs that you used in GeoQuiz.

In Chapter 7, you learned about classes that define a contract between you and the Activity you are starting. This contract defines the input you provide to start the Activity and the output you expect to receive as a result. There, you used the contract ActivityResultContracts.StartActivityForResult() – a basic contract that takes in an Intent and provides an ActivityResult as output.

You could use ActivityResultContracts.StartActivityForResult() again. But instead, you will use the more specific ActivityResultContracts.PickContact() class. It is a better option here because, as its name indicates, it is specifically designed for this use case.

ActivityResultContracts.PickContact() will send the user to an activity where they can select a contact. Once the user selects a contact, you will receive a Uri back as the result. You will see how to read the contact data from this Uri later in this chapter.

You expect a result back from the started activity, so you will use registerForActivityResult(…) again. In CrimeDetailFragment.kt, add the following:

Listing 16.10  Registering for a result (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    private val crimeDetailViewModel: CrimeDetailViewModel by viewModels {
        CrimeDetailViewModelFactory(args.crimeId)
    }

    private val selectSuspect = registerForActivityResult(
        ActivityResultContracts.PickContact()
    ) { uri: Uri? ->
        // Handle the result
    }
    ...
}

In onViewCreated(), set a click listener on the crimeSuspect button. Inside the listener, call the launch() function on your selectSuspect property. Unlike the work you did in Chapter 7, selecting a contact requires no input, so pass null into the launch() function.

Listing 16.11  Sending an implicit intent (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.apply {
            ...
            crimeSolved.setOnCheckedChangeListener { _, isChecked ->
                crimeDetailViewModel.updateCrime { oldCrime ->
                    oldCrime.copy(isSolved = isChecked)
                }
            }

            crimeSuspect.setOnClickListener {
                selectSuspect.launch(null)
            }
        }
    }
    ...
}

Next, modify updateUi(crime: Crime) to set the text on the CHOOSE SUSPECT button if the crime has a suspect. Use the String.ifEmpty() extension function to provide default text if there is no current suspect.

Listing 16.12  Setting CHOOSE SUSPECT button text (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    private fun updateUi(crime: Crime) {
        binding.apply {
            ...
            crimeReport.setOnClickListener {
                ...
            }

            crimeSuspect.text = crime.suspect.ifEmpty {
                getString(R.string.crime_suspect_text)
            }
        }
    }
    ...
}

Run CriminalIntent on a device that has a contacts app – use the emulator if your Android device does not have one. If you are using the emulator, add a few contacts using its Contacts app before you run CriminalIntent. Then run your app.

Press the CHOOSE SUSPECT button. You should see a list of contacts (Figure 16.5).

Figure 16.5  A list of possible suspects

A list of possible suspects

If you have a different contacts app installed, your screen will look different. Again, this is one of the benefits of implicit intents. You do not have to know the name of the contacts application to use it from your app. Users can install whatever app they like best, and the OS will find and launch it.

Getting data from the contacts list

Now you need to get a result back from the contacts application. Contacts information is shared by many applications, so Android provides an in-depth API for working with contacts information through a ContentProvider. Instances of this class wrap databases and make the data available to other applications. You can access a ContentProvider through a ContentResolver.

(The contacts database is a large topic in itself. We will not cover it here. If you would like to know more, read the Content Provider API guide at developer.android.com/​guide/​topics/​providers/​content-provider-basics.)

Because you started the activity with the ActivityResultContracts.PickContact() class, you might receive a data Uri as output. (We say might here because if the user cancels and does not select a suspect, your output will be null.) The Uri is not your suspect’s name or any data about them; rather, it points at a resource you can query for that information.

In CrimeDetailFragment.kt, add a function to retrieve the contact’s name from the contacts application. This is a lot of new code; we will explain it step by step after you enter it.

Listing 16.13  Pulling the contact’s name out (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment(), DatePickerFragment.Callbacks {
    ...
    private fun getCrimeReport(crime: Crime): String {
        ...
    }

    private fun parseContactSelection(contactUri: Uri) {
        val queryFields = arrayOf(ContactsContract.Contacts.DISPLAY_NAME)

        val queryCursor = requireActivity().contentResolver
            .query(contactUri, queryFields, null, null, null)

        queryCursor?.use { cursor ->
            if (cursor.moveToFirst()) {
                val suspect = cursor.getString(0)
                crimeDetailViewModel.updateCrime { oldCrime ->
                    oldCrime.copy(suspect = suspect)
                }
            }
        }
    }
    ...
}

In Listing 16.13, you create a query that asks for all the display names of the contacts in the returned data. Then you query the contacts database and get a Cursor object to work with. The Cursor points to a database table containing a single row and a single column. The row represents the contact the user selected, and the specified column has the contact’s name.

The Cursor.moveToFirst() function accomplishes two things for you: It moves the cursor to the first row, and it returns a Boolean you use to determine whether there is data to read from. To extract the suspect’s name, you call Cursor.getString(Int), passing in 0, to pull the contents of the first column in that first row as a string. Finally, you update the crime within your CrimeDetailViewModel.

Now, the suspect information is stored in the CrimeDetailViewModel, and your UI will update as it observes the StateFlow’s changes.

But there is one more step: You need to call parseContactSelection(Uri) when you get a result back. Invoke it when calling registerForActivityResult(…).

Listing 16.14  Invoking your function (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    private val selectSuspect = registerForActivityResult(
        ActivityResultContracts.PickContact()
    ) { uri: Uri? ->
        // Handle the result
        uri?.let { parseContactSelection(it) }
    }
    ...
}

Run your app, select a crime, and pick a suspect. The name of the suspect you chose should appear on the CHOOSE SUSPECT button. Then send a crime report. The suspect’s name should appear in the crime report (Figure 16.6).

Figure 16.6  Suspect name on button and in crime report

Suspect name on button and in crime report

Contacts permissions

You might be wondering how you are getting permission to read from the contacts database. The contacts app is extending its permissions to you.

The contacts app has full permissions to the contacts database. When the contacts app returns a data URI as the result, it also adds the flag Intent.FLAG_GRANT_READ_URI_PERMISSION. This flag signals to Android that CriminalIntent should be allowed to use this data one time. This works well, because you do not really need access to the entire contacts database. You only need access to one contact inside that database.

Checking for responding activities

The first implicit intent you created in this chapter will always be responded to in some way – there may be no way to send a report, but the chooser will still display properly. However, that is not the case for the second example: Some devices or users may not have a contacts app. This is a problem, because if the OS cannot find a matching activity, then the app will crash.

To determine whether your user has an appropriate contacts application, you will need to query the system to determine which activities will respond to your implicit intent. If one or more activities are returned, then your user is all set to pick a contact. If no activities come back, then the user does not have an appropriate contact picker and the functionality should be disabled in CriminalIntent.

Disclosing queries

To successfully make that query, you must first disclose that you are going to make it. This provides extra security for users, because apps have to declare what types of external requests they make to the system. In the past, scummy apps would abuse the ability to query for apps in order to fingerprint, or uniquely identify, the device. Those apps could then use that fingerprint to track that device across apps.

To prevent this invasion of privacy, you provide this disclosure within your AndroidManifest.xml. Open the file and make the following updates:

Listing 16.15  Adding external queries to manifest (AndroidManifest.xml)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.bignerdranch.android.criminalintent">

    <application ...>
        ...
    </application>
    <queries>
        <intent>
            <action android:name="android.intent.action.PICK" />
            <data android:mimeType="vnd.android.cursor.dir/contact" />
        </intent>
    </queries>
</manifest>

The queries block at the end of the manifest includes all the external intents the app is going to look up. Because CriminalIntent wants to check for a contacts app, the relevant intent information is provided so the system is aware. If you do not provide this disclosure, then on newer versions of Android the system will always tell you that no activities can handle your request.

Querying the PackageManager

Now that you have provided your disclosure, you can determine whether the OS can handle your request through the PackageManager class. PackageManager knows about all the components installed on an Android device, including all its activities. By calling resolveActivity(Intent, Int), you can ask it to find an activity that matches the Intent you gave it. The MATCH_DEFAULT_ONLY flag restricts this search to activities with the CATEGORY_DEFAULT flag, just like startActivity(Intent) does.

If this search is successful, it will return an instance of ResolveInfo telling you all about which activity it found. On the other hand, if the search returns null, the game is up – no app can handle your Intent. You can use this knowledge to enable or disable features, such as selecting a suspect from the list of contacts, based on whether the system can handle the request.

Add the canResolveIntent() function to the bottom of CrimeDetailFragment. It will take in an Intent and return a Boolean indicating whether that Intent can be resolved.

Listing 16.16  Resolving Intents (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    private fun parseContactSelection(contactUri: Uri) {
        ...
    }

    private fun canResolveIntent(intent: Intent): Boolean {
        val packageManager: PackageManager = requireActivity().packageManager
        val resolvedActivity: ResolveInfo? =
            packageManager.resolveActivity(
                intent,
                PackageManager.MATCH_DEFAULT_ONLY
            )
        return resolvedActivity != null
    }
}

Under the hood, the Activity Results API uses Intents to perform its actions. You can create an instance of those Intents by invoking createIntent() on the launcher’s contract property. Use your new canResolveIntent() function with the Intent backing the selectSuspect property to enable or disable the suspect button in onViewCreated(…). That way, the device will not crash if the user tries to select a suspect when the device does not have a contacts app.

Listing 16.17  Guarding against no contacts app (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.apply {
            ...
            crimeSuspect.setOnClickListener {
                selectSuspect.launch(null)
            }

            val selectSuspectIntent = selectSuspect.contract.createIntent(
                requireContext(),
                null
            )
            crimeSuspect.isEnabled = canResolveIntent(selectSuspectIntent)
        }
    }
    ...
}

If you would like to verify that your filter works, but you do not have a device without a contacts application, temporarily add an additional category to the intent trying to be resolved. This category does nothing, but it will prevent any contacts applications from matching your intent.

Listing 16.18  Adding dummy code to verify filter (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    private fun canResolveIntent(intent: Intent): Boolean {
        intent.addCategory(Intent.CATEGORY_HOME)
        val packageManager: PackageManager = requireActivity().packageManager
        val resolvedActivity: ResolveInfo? =
            packageManager.resolveActivity(
                intent,
                PackageManager.MATCH_DEFAULT_ONLY
            )
        return resolvedActivity != null
    }
}

Run CriminalIntent again, and you should see the CHOOSE SUSPECT button disabled (Figure 16.7).

Figure 16.7  Disabled CHOOSE SUSPECT button

Disabled CHOOSE SUSPECT button

Delete the dummy code once you are done verifying this behavior.

Listing 16.19  Deleting dummy code (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    private fun canResolveIntent(intent: Intent): Boolean {
        intent.addCategory(Intent.CATEGORY_HOME)
        val packageManager: PackageManager = requireActivity().packageManager
        val resolvedActivity: ResolveInfo? =
            packageManager.resolveActivity(
                intent,
                PackageManager.MATCH_DEFAULT_ONLY
            )
        return resolvedActivity != null
    }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset