school-it.pl // android + kotlin // rozdział 8 // lekcja 34
Intencje niejawne
Twoja aplikacja nie musi umieć dzwonić, wyświetlać stron ani pokazywać map — w telefonie są już aplikacje, które robią to świetnie. Wystarczy powiedzieć Androidowi, co chcesz zrobić („zadzwoń pod ten numer”), a nie który ekran otworzyć. Taka intencja nazywa się niejawna. Zbudujesz wizytówkę szkoły z przyciskami: zadzwoń, napisz e-mail, otwórz stronę, pokaż na mapie i udostępnij.
Czego się dziś nauczysz
- Odróżnisz intencję jawną (konkretny ekran) od niejawnej (czynność).
- Otworzysz wybieranie numeru, przeglądarkę, mapę i program pocztowy.
- Zbudujesz adres
Uridla każdej czynności. - Udostępnisz tekst z oknem wyboru aplikacji (
createChooser). - Zabezpieczysz aplikację przed brakiem odpowiedniej aplikacji:
try/catch.
Jawna a niejawna
| Intencja jawna (lekcje 30–33) | Intencja niejawna (ta lekcja) | |
|---|---|---|
| co podajesz | klasę ekranu: Intent(this, DzwonkiActivity::class.java) | czynność i dane: Intent(Intent.ACTION_DIAL, Uri.parse("tel:…")) |
| kto otwiera | zawsze ten jeden ekran twojej aplikacji | aplikacja w telefonie, która potrafi to zrobić |
| przykład z życia | „Ola, przynieś kredę” | „Niech ktoś przyniesie kredę” |
pod 59 000 00 00”
która to obsługuje
numerem
Gdy pasuje kilka aplikacji (np. dwie przeglądarki), Android zapyta użytkownika, której użyć. Gdy nie pasuje żadna — zgłosi błąd, który trzeba przechwycić (sekcja „Gdy nie ma takiej aplikacji”).
Akcja i adres (Uri)
Intencja niejawna ma dwie części: akcję (co zrobić) i adres danych typu Uri (na czym). Początek adresu, np. tel: czy https:, mówi Androidowi, jakiego rodzaju to dane.
| Czynność | Akcja | Adres Uri |
|---|---|---|
| wybierz numer | Intent.ACTION_DIAL | "tel:590000000" |
| otwórz stronę | Intent.ACTION_VIEW | "https://school-it.pl" |
| pokaż na mapie | Intent.ACTION_VIEW | "geo:0,0?q=" + Uri.encode("Lębork, Polska") |
| napisz e-mail | Intent.ACTION_SENDTO | "mailto:sekretariat@szkola.example" |
// numer pojawi się w aplikacji Telefon — dzwoni dopiero użytkownik
startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:590000000")))
// ta sama akcja VIEW — o aplikacji decyduje początek adresu
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://school-it.pl")))
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("geo:0,0?q=" + Uri.encode("Lębork, Polska"))))
// e-mail z gotowym tematem
val mail = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:sekretariat@szkola.example"))
mail.putExtra(Intent.EXTRA_SUBJECT, "Pytanie o rekrutację")
startActivity(mail)
Uri.encode zamienia spacje i polskie litery na zapis bezpieczny w adresie. Bez niego adres z nazwą ulicy może się „urwać” na pierwszej spacji.
DIAL, a nie CALL
Istnieje też ACTION_CALL, która dzwoni od razu. Wymaga jednak uprawnienia CALL_PHONE i zgody użytkownika w czasie działania aplikacji. ACTION_DIAL tylko wpisuje numer — przycisk połączenia naciska użytkownik — i nie wymaga żadnych uprawnień. W prawie każdej aplikacji to właściwy wybór.
Udostępnianie tekstu
„Udostępnij” wysyła tekst do dowolnej aplikacji: komunikatora, poczty, notatek. Tu nie ma adresu — jest typ danych i sam tekst w extras:
val intencja = Intent(Intent.ACTION_SEND)
intencja.type = "text/plain" // wysyłamy zwykły tekst
intencja.putExtra(Intent.EXTRA_TEXT, "Dzień otwarty: 20 marca, godz. 10:00")
startActivity(Intent.createChooser(intencja, "Udostępnij przez"))
Intent.createChooser zawsze pokazuje systemowe okno wyboru aplikacji z podanym tytułem — także wtedy, gdy użytkownik kiedyś zaznaczył „zawsze używaj tej aplikacji”. Przy udostępnianiu właśnie tego chcemy.
Gdy nie ma takiej aplikacji
Na emulatorze bez Map Google albo na tablecie bez aplikacji Telefon startActivity zgłasza wyjątek ActivityNotFoundException i aplikacja się zamyka. Wyjątek to „zgłoszony błąd”, który można przechwycić i obsłużyć po swojemu:
private fun uruchom(intencja: Intent) {
try {
startActivity(intencja) // spróbuj…
} catch (e: ActivityNotFoundException) {
// …a gdy się nie uda — zamiast awarii komunikat
Toast.makeText(this, "Brak aplikacji, która to obsłuży", Toast.LENGTH_SHORT).show()
}
}
może się nie udać
wykonaj ten kod zamiast awarii
Wszystkie przyciski w przykładzie wywołują uruchom(…) — obsługa braku aplikacji jest napisana raz.
Wizytówka szkoły
Ekran z nazwą szkoły, adresem, telefonem i e-mailem. Pięć przycisków uruchamia intencje niejawne: wybieranie numeru, e-mail z tematem, stronę WWW, mapę i udostępnienie danych kontaktowych. Brak aplikacji kończy się komunikatem, nie awarią.
lista aplikacji w oknie wyboru zależy od telefonu — Android pokazuje te, które przyjmują tekst
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Wizytowka, Kotlin, API 24. - Podmień
activity_main.xmliMainActivity.kt. Manifestu nie trzeba zmieniać — nie otwieramy własnych ekranów i nie potrzebujemy uprawnień. - Do testów użyj emulatora z obrazem Google Play albo prawdziwego telefonu — tam są aplikacje Telefon, Chrome i Mapy.
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvNazwa"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvAdres"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:textSize="14sp" />
<TextView
android:id="@+id/tvKontakt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:lineSpacingExtra="4dp"
android:textSize="16sp" />
<Button
android:id="@+id/btnZadzwon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Zadzwoń" />
<Button
android:id="@+id/btnEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Napisz e-mail" />
<Button
android:id="@+id/btnStrona"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Otwórz stronę szkoły" />
<Button
android:id="@+id/btnMapa"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pokaż na mapie" />
<Button
android:id="@+id/btnUdostepnij"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Udostępnij kontakt" />
</LinearLayout>
</ScrollView>
package pl.schoolit.wizytowka
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
// ---------- kontrolki ----------
private lateinit var tvNazwa: TextView
private lateinit var tvAdres: TextView
private lateinit var tvKontakt: TextView
private lateinit var btnZadzwon: Button
private lateinit var btnEmail: Button
private lateinit var btnStrona: Button
private lateinit var btnMapa: Button
private lateinit var btnUdostepnij: Button
// ---------- dane szkoły (przykładowe) ----------
private val NAZWA = "Technikum nr 1"
private val ADRES = "ul. Szkolna 1, Lębork"
private val TELEFON = "590000000"
private val EMAIL = "sekretariat@szkola.example"
private val STRONA = "https://school-it.pl"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initUI()
ustawZdarzenia()
pokazDane()
}
private fun initUI() {
tvNazwa = findViewById(R.id.tvNazwa)
tvAdres = findViewById(R.id.tvAdres)
tvKontakt = findViewById(R.id.tvKontakt)
btnZadzwon = findViewById(R.id.btnZadzwon)
btnEmail = findViewById(R.id.btnEmail)
btnStrona = findViewById(R.id.btnStrona)
btnMapa = findViewById(R.id.btnMapa)
btnUdostepnij = findViewById(R.id.btnUdostepnij)
}
private fun ustawZdarzenia() {
btnZadzwon.setOnClickListener { zadzwon() }
btnEmail.setOnClickListener { napiszEmail() }
btnStrona.setOnClickListener { otworzStrone() }
btnMapa.setOnClickListener { pokazMape() }
btnUdostepnij.setOnClickListener { udostepnij() }
}
// ---------- intencje niejawne ----------
private fun zadzwon() {
val intencja = Intent(Intent.ACTION_DIAL, Uri.parse("tel:$TELEFON"))
uruchom(intencja)
}
private fun napiszEmail() {
val intencja = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:$EMAIL"))
intencja.putExtra(Intent.EXTRA_SUBJECT, "Pytanie z aplikacji szkolnej")
uruchom(intencja)
}
private fun otworzStrone() {
uruchom(Intent(Intent.ACTION_VIEW, Uri.parse(STRONA)))
}
private fun pokazMape() {
val adresMapy = Uri.parse("geo:0,0?q=" + Uri.encode(ADRES)) // spacje i „ę” zakodowane
uruchom(Intent(Intent.ACTION_VIEW, adresMapy))
}
private fun udostepnij() {
val tekst = "$NAZWA\n$ADRES\ntel. ${telefonCzytelny()}\n$EMAIL\n$STRONA"
val intencja = Intent(Intent.ACTION_SEND)
intencja.type = "text/plain"
intencja.putExtra(Intent.EXTRA_TEXT, tekst)
uruchom(Intent.createChooser(intencja, "Udostępnij przez"))
}
// jedno miejsce, które chroni przed brakiem aplikacji
private fun uruchom(intencja: Intent) {
try {
startActivity(intencja)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, "Brak aplikacji, która to obsłuży", Toast.LENGTH_SHORT).show()
}
}
// ---------- dane → ekran ----------
// "590000000" → "59 000 00 00"
private fun telefonCzytelny(): String =
"${TELEFON.substring(0, 2)} ${TELEFON.substring(2, 5)} ${TELEFON.substring(5, 7)} ${TELEFON.substring(7)}"
private fun pokazDane() {
tvNazwa.text = NAZWA
tvAdres.text = ADRES
tvKontakt.text = "📞 ${telefonCzytelny()}\n✉️ $EMAIL\n🌐 $STRONA"
}
}
Co dzieje się po kolei
- Dane szkoły są w polach — zmiana numeru czy adresu to zmiana jednej linii, a nie szukanie po całym kodzie.
- Każdy przycisk buduje intencję z akcją i adresem
Uri, a potem przekazuje ją douruchom(). - Zadzwoń: aplikacja Telefon otwiera się z wpisanym numerem; użytkownik sam decyduje, czy dzwonić.
- Napisz e-mail:
ACTION_SENDTOz adresemmailto:otwiera tylko programy pocztowe; temat jest już wpisany. - Pokaż na mapie:
geo:0,0?q=…to „wyszukaj ten adres”.Uri.encodechroni przed spacjami i polskimi literami. - Udostępnij: tekst w
EXTRA_TEXT, typtext/plaini okno wyboru aplikacji. - Gdy żadna aplikacja nie przyjmie intencji,
catchpokazuje Toast zamiast awarii.
Sprawdź trzy rzeczy
- Kliknij Zadzwoń. Numer jest wpisany, ale połączenie się nie zaczęło — tak działa
ACTION_DIAL. - Zmień adres strony na
"school-it.pl"(bezhttps://). Zobaczysz komunikat o braku aplikacji: bez początku adresu Android nie wie, że to strona WWW. - W
uruchom()usuńtry/catch(zostaw samostartActivity) i powtórz punkt 2. Teraz aplikacja się zamknie. Przywróćtry/catchi poprawny adres.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
Intent(akcja, uri) | Intent | Intencja niejawna: czynność i dane. |
Intent.ACTION_DIAL + tel: | akcja | Wpisuje numer w aplikacji Telefon. |
Intent.ACTION_VIEW + https: / geo: | akcja | Pokazuje stronę lub miejsce na mapie. |
Intent.ACTION_SENDTO + mailto: | akcja | Nowa wiadomość e-mail do adresata. |
Intent.ACTION_SEND + type = "text/plain" | akcja | Udostępnianie tekstu. |
Intent.EXTRA_SUBJECT, Intent.EXTRA_TEXT | klucze | Gotowe klucze: temat i treść. |
Intent.createChooser(intencja, "tytuł") | Intent | Okno wyboru aplikacji. |
Uri.parse("…") | Uri | Zamienia napis na adres danych. |
Uri.encode("…") | String | Koduje spacje i znaki specjalne w adresie. |
try { … } catch (e: ActivityNotFoundException) { … } | składnia | Przechwytuje brak aplikacji zamiast awarii. |
napis.substring(od, do) | String | Fragment napisu od pozycji od do do (bez niej). |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
aplikacja zamyka się, Logcat: ActivityNotFoundException: No Activity found to handle Intent | Brak aplikacji dla tej czynności (np. Map na emulatorze) lub zły adres. Uruchamiaj przez try/catch. |
Uri.parse("school-it.pl") nic nie otwiera | Adres strony musi zaczynać się od https:// (lub http://). |
Uri.parse("590000000") dla telefonu | Brak przedrostka tel:. |
ACTION_CALL → awaria SecurityException | Wymaga uprawnienia CALL_PHONE. Użyj ACTION_DIAL. |
| mapa szuka tylko „ul.” | Adres ze spacjami bez Uri.encode. |
| udostępnianie bez typu → brak aplikacji | Przy ACTION_SEND ustaw type = "text/plain". |
sprawdzanie intencja.resolveActivity(packageManager) != null zawsze daje „brak” | Od Androida 11 aplikacja nie widzi innych aplikacji bez wpisu <queries> w manifeście. Prościej: try/catch. |
Zadania
Każde zadanie wykonaj w nowym projekcie Empty Views Activity. Każdą intencję uruchamiaj przez metodę z try/catch. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Pole tekstowe i przycisk Szukaj. Kliknięcie otwiera w przeglądarce wyniki wyszukiwania wpisanej frazy.
pokaż wskazówkę
Uri.parse("https://duckduckgo.com/?q=" + Uri.encode(fraza))zACTION_VIEW.
Trzy duże przyciski: „112 — numer alarmowy”, „999 — pogotowie”, „998 — straż pożarna”. Każdy wpisuje numer w aplikacji Telefon (nie dzwoni od razu!).
pokaż wskazówkę
- Jedna metoda
wybierz(numer: String)wywoływana z trzech przycisków.
Dołóż do dowolnej wcześniejszej aplikacji z wynikiem (np. quiz z lekcji 27 lub kalkulator BMI z lekcji 31) przycisk Pochwal się, który udostępnia tekst „Mój wynik: 4/5 w quizie z Androida!”.
pokaż wskazówkę
ACTION_SEND,type = "text/plain",EXTRA_TEXT,createChooser.
Formularz: numer sali (Spinner), opis usterki (wieloliniowy EditText). Wyślij otwiera program pocztowy z adresem administratora, tematem „Usterka — sala 12” i opisem w treści.
pokaż wskazówkę
- Treść:
putExtra(Intent.EXTRA_TEXT, opis)obokEXTRA_SUBJECT. Gdy opis pusty —errorprzy polu i bez wysyłania.
ListView z 6 ciekawymi miejscami w twoim mieście. Kliknięcie wiersza pokazuje miejsce na mapie, przytrzymanie — udostępnia jego nazwę i adres.
pokaż wskazówkę
- Dwie tablice (nazwy i adresy) albo klasa danych z lekcji 28. Mapa:
"geo:0,0?q=" + Uri.encode("$nazwa, $adres").
Połącz lekcje 29 i 34: lista kontaktów z własnym adapterem. W każdym wierszu imię, numer oraz dwa ImageButton: słuchawka (ACTION_DIAL) i koperta (ACTION_SENDTO z smsto: — nowy SMS na ten numer). Kliknięcie wiersza udostępnia kontakt jako tekst.
pokaż wskazówkę
- Adapter nie uruchamia intencji sam — przekaż mu funkcje
zadzwon: (String) -> Unitisms: (String) -> Unitz aktywności (lekcja 29). SMS:Uri.parse("smsto:$numer").
Co trzeba zapamiętać
- Intencja niejawna = akcja + dane; Android sam wybiera aplikację.
ACTION_DIAL+tel:,ACTION_VIEW+https:/geo:,ACTION_SENDTO+mailto:.- Udostępnianie:
ACTION_SEND,type,EXTRA_TEXT,createChooser. - Tekst w adresie koduj przez
Uri.encode. - Zawsze
try/catch (ActivityNotFoundException)— nie każdy telefon ma każdą aplikację.
To koniec rozdziału o intencjach. W następnym: menu na pasku aplikacji, cykl życia aktywności i zapisywanie ustawień.
Dokumentacja: Typowe intencje, Udostępnianie danych.