Angular get value from NgModel

Hello I want to send multiple values from a to an api function.

on the db i store the values in text format. The value that is stored is array

on the Swal alert, I am getting [object object] but i want to get each value e.g. Painting or Graphic Design.

Here is my code so far.

html

<ion-item>
  <ion-label>Painting</ion-label>
  <ion-toggle color="gold" [(ngModel)]="creative.Painting" (click)="creativeInterest(creative)"></ion-toggle>
</ion-item>

<ion-item>
  <ion-label>Graphic Design</ion-label>
  <ion-toggle color="tertiary" [(ngModel)]="creative.Graphic Design" (click)="creativeInterest(creative)"></ion-toggle>
</ion-item>

.ts

   export class CreativeSettingsPage implements OnInit {
    creative: any = {};
    userDetails: any = {};
    constructor(
      public userData: UserData
  ) {
    this.userDetails = this.userData.getUserData();
   }

  ngOnInit() {
  }

  creativeInterest(creative:string)
  {
    this.userData.creativeSettings(this.userDetails.uid, creative).pipe(
      map((data: any) => {
        if (data.success) {
          Swal.fire({
            icon: 'success',
            title: creative,
            showConfirmButton: false,
            backdrop: false,
            timer: 2500
          })
        }
      })
    ).subscribe()
  }

user-data.ts

    creativeSettings(uid: number, creative:any) {
        const url = this.appData.getApiUrl() + 'creativeSettings';
        const data = this.jsonToURLEncoded({
            uid: uid,
            creative: creative
        });
        return this.http.post(url, data, { headers: this.options });
    }

php

function creativeSettings()
{
$request = \Slim\Slim::getInstance()->request();
$response['success'] = true; // 1 true if not errors OK NOTHING

$uid = $request->post('uid');
$creative_interests =  $request->post('creative');

$db = getDB();
$sql = "UPDATE users SET creative_interests = :creative_interests WHERE uid = :uid";
$stmt = $db->prepare($sql);
$stmt->bindParam("uid", $uid);
$stmt->bindParam("creative_interests", $creative_interests);
$stmt->execute();
$db = null;

echo json_encode($response);

}

Android Native – Open documents via the Storage Access Framework

Introduction

The Storage Access Framework (SAF) provides a great way to access files exposed by other applications via their own DocumentProviders. In this tutorial, we will learn how to use the SAF in our App.

Goals

At the end of the tutorial, you would have learned:

  1. How to open documents via the Storage Access Framework.
Tools Required

Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.

Prerequisite Knowledge
  1. Intermediate Android.
  2. ActivityResult API.
  3. Intents
Project Setup

To follow along with the tutorial, perform the steps below:

  1. You are commended to use a test device or AVD image with the Google Play Store pre-installed.

  2. Install Adobe Acrobat Reader: Edit PDF

  3. Using the default Chrome browser on your AVD, download a sample PDF file from this projects Github repository here. This ensures that the downloaded file will be written to the MediaStore.Downloads table.

  4. Create a new Android project with the default Empty Activity.

  5. Delete the default Hello World! TextView.

  6. Add a new Button to ConstraintView.

  7. Constraint the Button to the center of the screen.

  8. Extract the android:text of the Button to strings.xml with the value of Open PDF.

  9. Your acitivty_main.xml file should now look similar to this.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/open_pdf"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>
Project Overview

Our project is quite simple. It only has a single Button View. When completed, we expect our App to successfully provide the workflow below:

  1. The end user taps on the button.
  2. Android provides the user with a Picker to select PDF files.
  3. The end user selects a PDF file in the Picker.
  4. Our App starts an implicit Intent to let Android decide which App to handle the Intent.
  5. The user selects the Adobe PDF reader to read the PDF file.
  6. Android opens the Adobe PDF reader, passing in the URI for the file selected previously at step 3.
  7. The user can now view the PDF file.
Storage Access Framework Concepts

The SAF has three components:

  1. Document provider: a class that implements DocumentsProvider. This class can provide read and write access to files either locally or cloud-based. It must also include the proper <provider> tag in its manifest. Android already provides some default DocumentsProvider for us to use, so we do not have to implement them by ourselves in this tutorial. When the file Picker is shown, all registered DocumentProviders will be listed. In the screenshot below, each of the listings in red is a DocumentProvider.
    1.jpg
  2. Client: an App that will make use of the Document Provider via the intent actions ACTION_CREATE_DOCUMENT, ACTION_OPEN_DOCUMENT, or ACTION_OPEN_DOCUMENT_TREE. For this tutorial, we will not use Intents and these intent actions directly, instead we will leverage the premade AcitvityResultContract implementations from ActivityResultContracts.
  3. Picker: A file Picker UI provided by the system, so we do not have to implement it by ourselves either.
Open the file Picker

With the basic concepts out of the way, it is time to write some code. The first thing that we would need to do is to launch the file Picker with our App.

  1. Append to MainActivity#onCreate with the code below to create an ActivityResultLauncher and an empty callback. Notice that we used the premade ActivityResultContract implementation ActivityResultContracts.OpenDocument here.

     val openPdfLauncher = registerForActivityResult(OpenDocument()) { uri ->
    
     }
  2. Retrieve the Button object.

     //Gets a reference to the Button object
     val button = findViewById<Button>(R.id.button)
  3. Binds the Buttons onClickListener to launch(). You can pass a String array to filter out files by MIME types. The first two commented out array declarations are just examples to give you some ideas.

     //Binds button to start the file Picker
     button.setOnClickListener {
        //val input = emptyArray<String>() //this will match nothing.
        //val input = arrayOf("text/plain") //if you want to filter .txt files
        val input = arrayOf("application/pdf")
        openPdfLauncher.launch(input)
     }
Performs an action on the returned Uri

The code provided so far will only launch the file Picker and then return to our App, doing nothing else. To read the PDF file with an external App, add the code below inside of the openPdfLauncher callback. Notice my comment regarding the use of the FLAG_GRANT_READ_URI_PERMISSION below, so you should be careful when passing the permission via an implicit intent.

//Creates the Intent object, specifying the uri and mime type
val openPdfIntent = Intent().apply {
   action = Intent.ACTION_VIEW
   type = "application/pdf"
   data = uri
   //grant the handler permission to read the Uri.
   //Should sanitize to avoid leaking sensitive user information.
   flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}

//Asks the system to start another activity that can handle Intent
startActivity(openPdfIntent)
Run the App

We are now ready to run the App. You should be able to replicate the expected user flow like in the animation below.

OpenPDF.gif

Solution Code

MainActivity.kt

package com.example.daniwebandroidstorageaccessframework

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import androidx.activity.result.contract.ActivityResultContracts.OpenDocument

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val openPdfLauncher = registerForActivityResult(OpenDocument()) { uri ->


           //Creates the Intent object, specifying the uri and mime type
           val openPdfIntent = Intent().apply {
               action = Intent.ACTION_VIEW
               type = "application/pdf"
               data = uri
               //grant the handler permission to read the Uri.
               //Should sanitize to avoid leaking sensitive user information.
               flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
           }

           //Asks the system to start another activity that can handle Intent
           startActivity(openPdfIntent)
       }

       //Gets a reference to the Button object
       val button = findViewById<Button>(R.id.button)

       //Binds button to start the file Picker
       button.setOnClickListener {
           //val input = emptyArray<String>() //this will match nothing.
           //val input = arrayOf("text/plain") //if you want to filter .txt files
           val input = arrayOf("application/pdf")
           openPdfLauncher.launch(input)
       }

   }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/open_pdf"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Storage Access Framework</string>
   <string name="open_pdf">Open PDF</string>
</resources>
Summary

We have learned how to open a document using the Storage Access Framework. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidStorageAccessFramework

Android Native – Play videos using VideoView

Introduction

If your App only need a simple way to play videos on Android, then VideoView (android.widget.VideoView) might just fit the bill. In this tutorial, we will learn how to use VideoView for video playback in our App.

Goals

At the end of the tutorial, you would have learned:

  1. How to use VideoView to play videos.
Tools Required
  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
  1. Intermediate Android.
  2. Storage Access Framework (SAF).
  3. ActivityResult APIs.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Remove the default Hello World! TextView.

  3. Add a new Button to ConstraintLayout.

  4. Constraint it to the top, left and right of the screen.

  5. Give the Button 64dp margin from the top of the screen.

  6. Extract the Buttons android:text value to strings.xml with the value as Play Video.

  7. Your activity_main.xml should now look similar to this.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:app="http://schemas.android.com/apk/res-auto"
         xmlns:tools="http://schemas.android.com/tools"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         tools:context=".MainActivity">
    
         <Button
             android:id="@+id/button"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginTop="64dp"
             android:text="@string/button"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toTopOf="parent" />
    
     </androidx.constraintlayout.widget.ConstraintLayout>
  8. From the test devices web browser, download the sample video from this projects Github repository here. The sample video used here is the trailer of the short movie Sintel ( copyright Blender Foundation | durian.blender.org). Optionally, you can also drag and drop the video from your computer directly into the AVD.

Supported Media Formats

For Android to be able to playback a video file, the file format must either be supported by the Android system or the device vendor. The list of supported media formats by Android can be found here. To find out the supported Media MIME types on a device, we can use the various functions in the MediaCodecList class and the MediaCodecInfo class. For example, to list all supported(either encoding or decoding) MIME types on our AVD,

  1. Add the function below to MainActivity.

     private fun logCodecs(){
        val codecList = MediaCodecList(ALL_CODECS)
    
        for (codecInfo in codecList.codecInfos){
            for (mimeType in codecInfo.supportedTypes){
                Log.d(TAG, mimeType)
            }
        }
     }
  2. Add the top-level TAG constant.

     private const val TAG = "MAIN_ACTIVITY"
  3. Call the function in onCreate().

     logCodecs()

If you run your App now, it will log something similar to the content below (output has been formatted for readability).

audio/mp4a-latm
audio/3gpp
audio/amr-wb
audio/flac
audio/g711-alaw
audio/g711-mlaw
audio/mpeg
audio/opus
audio/raw
audio/vorbis
video/avc
video/x-vnd.on2.vp8
video/x-vnd.on2.vp9
video/av01
video/3gpp
video/hevc
video/mp4v-es
Preparing the VideoView
  1. To play a video with a VideoView, first we will add a VideoView in activity_main.xml.

     <VideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/button" />
  2. It looks a little bit better with some margin, so we will add 8dp of margins to videoView.

     android:layout_marginStart="8dp"
     android:layout_marginTop="8dp"
     android:layout_marginEnd="8dp"
     android:layout_marginBottom="8dp"
  3. We do not need to need to show the videoView until the video starts playing, so we will mark it as invisible at startup.

     android:visibility="invisible"
Open the Media file

Normally, only videos located under DCIM/, Movies/, and Pictures/ will be available in the MediaStore.Video table. Because our video file is located under /Downloads, we will interact with it via the SAF.

  1. Create the ActivityResultLauncher and an empty callback with the code below inside onCreate().

     val openVideoLauncher = registerForActivityResult(OpenDocument()) { uri ->
    
     }
  2. Retrieve the Button object.

     //Gets the button reference
     val button = findViewById<Button>(R.id.button)
  3. Bind the Button onClickListener to the file Picker, passing in an array of the video MIME types.

     button.setOnClickListener {
        val mimeTypes = arrayOf("video/mp4") //filters for mp4 videos
        openVideoLauncher.launch(mimeTypes)
     }
The VideoView object

The VideoView object provides convenient functions to make it super easy to play videos. To interact with it, follow the steps below.

  1. Just above openVideoLauncher, retrieves the VideoView object.

     //Gets the videoView reference
     val videoView = findViewById<VideoView>(R.id.videoView)
  2. Bind the code below to the openVideoLauncher callback. This code will set the URI object returned from ActivityResultLauncher as the source for the video, makes the VideoView visible, and then start the video.

     //Start the video now that we have the uri
     videoView.apply {
        setVideoURI(uri) //sets the video uri
        visibility = VISIBLE //makes videoView visible
     }.start()
Run the App

We are now ready to run our app. The workflow should be similar to the animation below.

SintelApp.gif

Solution Code

MainActivity.kt

package com.example.daniwebandroidplayvideo

import android.media.MediaCodecList
import android.media.MediaCodecList.ALL_CODECS
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View.VISIBLE
import android.widget.Button
import android.widget.VideoView
import androidx.activity.result.contract.ActivityResultContracts.OpenDocument

private const val TAG = "MAIN_ACTIVITY"

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       logCodecs()

       //Gets the videoView reference
       val videoView = findViewById<VideoView>(R.id.videoView)

       val openVideoLauncher = registerForActivityResult(OpenDocument()) { uri ->
           //Start the video now that we have the uri
           videoView.apply {
               setVideoURI(uri) //sets the video uri
               visibility = VISIBLE //makes videoView visible
           }.start()
       }

       //Gets the button reference
       val button = findViewById<Button>(R.id.button)

       button.setOnClickListener {
           val mimeTypes = arrayOf("video/mp4") //filters for mp4 videos
           openVideoLauncher.launch(mimeTypes)
       }

   }

   private fun logCodecs(){
       val codecList = MediaCodecList(ALL_CODECS)

       for (codecInfo in codecList.codecInfos){
           for (mimeType in codecInfo.supportedTypes){
               Log.d(TAG, mimeType)
           }
       }
   }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_marginTop="64dp"
       android:text="@string/button"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <VideoView
       android:id="@+id/videoView"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_marginStart="8dp"
       android:layout_marginTop="8dp"
       android:layout_marginEnd="8dp"
       android:layout_marginBottom="8dp"
       android:visibility="invisible"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@id/button" />

</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Play Video</string>
   <string name="button">Play Video</string>
</resources>
Summary

We have learned how to play a video using the VideoView. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidPlayVideoVideoView

ANDROID STUDIO The System cannot find the path specified

I have updated my Android Studio to the Artic Fox Version. After that neither the projects with old Gradle versions running nor the new projects with latest Gradle versions running. Only the following errors appears: Could not install Gradle distribution from 'https: //services.gradle.org/distributions/gradle-7.0.2-bin.zip'

After this error I manually downloaded the Gradle 7.0.2, extracted it and Set it as Gradle path in Android Studio > File>Settings>Gradle and choose the option "Use Gradle from Specified Location". But all in vain. Only I can see the following message, when I try to Sync project with Gradle.

"The system cannot find the path specified." I have been searching this problem for two days on internet, but nothing worked out for me.

Current Path where I have downloaded and extracted the Gradle Distribution is follows F:/Android Gradle/gradle-7.0.2

What mistake I am making?

Unable to resolve firebase 17.0.4

Hi DW, I'm experiencing an issue I've tried invalidating, fixed the offline sync but still I'm getting this error. My App uses Firebase FCM.

On the dependencies I have com.google.firebase:firebase-core:16.0.5 com.google.firebase:firebase-messaging:17.3.4

My implementation com.android.support:appcompact-v7:28.0.0-alpha1

This implementation is underlined with a red line and I think it is what having the issue, i remember now when I had this kind of error before I saw a post that said I must change the version here but don't know to which version to change to. If anyone know how can I fix this error please help me out. Thanks

Android Native – Query Audio Files from MediaStore

Introduction

In Android development, the MediaStore API is a great API to use if you are building a music player. In this tutorial, we will learn how to query for audio files in the MediaStore database.

Goals

At the end of the tutorial, you would have learned:

  1. How to query for Audio files from the MediaStore.
Tools Required
  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
  1. Intermediate Android
  2. Android permissions.
  3. ActivityResult API.
  4. Basic SQL.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Remove default Hello World! TextView.

  3. Add a new Button under ConstraintLayout.

  4. Constraint the Button to the center of the screen.

  5. Extract the Button android:text value to strings.xml. Set the Resource Name to button_startQuery and Resource Value to Start Query.

  6. Your activity_main.xml content should be similar to the code below.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/button_startQuery"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  7. Download the .mp3 file called Unexpected Gifts of Spring by Alextree from the FreeMusicArchive. This song is licensed under CC BY 4.0.

Project Overview

The frontend of our project is quite simple. It only has a single button. When the project is completed, our App should be able to perform the workflow below:

  1. User taps the Starts Query button.
  2. If the App has not been granted the runtime(dangerous) permission of android.permission.READ_EXTERNAL_STORAGE, the App will request the user to grant this permission.
  3. If the App has received the permission, it will then query the MediaStore database for entries matching the query parameters.
  4. The App will log the query results via the debugging channel.
READ_EXTERNAL_STORAGE

Because our App workflow requires the READ_EXTERNAL_STORAGE permission, so we will take care of that first. Based on the documentation, this permission is a runtime permission, so we must manually request it ourselves (API 23 or higher).

To add the permission request into our workflow, performs the steps below:

  1. In AndroidManifest.xml, add the <uses-permission> element inside of <manifest>, but outside of <application>.

     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  2. Open up the file MainActivity.kt. Inside of onCreate(), add a reference to the Button object.

     val button = findViewById<Button>(R.id.button)
  3. Below the button reference, add the code below to define an ActivityResultLauncher with an empty callback.

     val permissionResultLauncher = registerForActivityResult(RequestPermission()){ isGranted ->
    
     }
  4. Add the code below to bind the Button onClickListener to the permissionResultLauncher#launch() action.

     button.setOnClickListener {
        permissionResultLauncher.launch(READ_EXTERNAL_STORAGE)
     }
  5. You can run the app now if you wish just to see how the permission flow looks like. To clear the granted permission, either reinstall the app or use the adb revoke command below:

     adb shell pm revoke com.example.daniwebandroidqueryaudiomediastore android.permission.READ_EXTERNAL_STORAGE
MediaStore

The Android system has common public locations for Apps to store media files, such as Audio, Video, or Image. For this tutorial, we are only concerned about audio files; Android will automatically scan for audio files in the locations below.

  • Alarms/
  • Audiobooks/
  • Music/
  • Notifications/
  • Podcasts/
  • Ringtones/
  • Recordings/

After scanning, it stores information about these files inside an internal database called MediaStore. To interact with this database, we can use the MediaStore API. When interacting with the MediaStore database, there are a couple of important things to keep in mind:

  1. MediaStore only automatically refreshes its data at certain times, like a cold boot.
  2. Apps can write entries to the MediaStore even if the actual files do not exist. A file can be deleted in a way that does not trigger MediaStore to update itself. Proper checking/error handling is recommended when attempting to play a media file.
MediaStore Query

Even though we are interacting with the MediaStore database via the MediaStore API, just like any SQL query, we will need to create our query. To do this, follow the steps below:

  1. Inside the MainActivity class, create a new queryAudio() function.

     private fun queryAudio(){
     }
  2. To send a SQL query to the MediaStore database, we would use the query() function from the ContentResolver object. We can retrieve the ContentResolver from the App Context. Add the code below into queryAudio().

     applicationContext.contentResolver.query(
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
        projection,
        selection,
        selectionArgs,
        sortOrder
     )
  3. The query() function used here takes 5 arguments. Refer to the list below for a simplified explanation for them.
    a. uri: the location to query.
    b. projection: the columns that you want to be included.
    c. selection: this is where you put the SQL WHERE clause.
    d. selectionArgs: if you have any parameter used in the selection query (the previous function argument), you can provide values for them here.
    e. sortOrder: how you would like the result to be sorted.

  4. Now let us define the arguments before the query() call.

     val projection = arrayOf(
        MediaStore.Audio.Media.TITLE,
        MediaStore.Audio.Media.ALBUM
     )
    
     val selection = null //not filtering out any row.
     val selectionArgs = null //this can be null because selection is also null
     val sortOrder = null //sorting order is not needed
Use the Cursor to navigate

The query() call returns a Cursor object, so we will need to use it to get the data.

  1. Link the result of the query() call to a use clause (similar to Java try-with-resource) using the code below.

     applicationContext.contentResolver.query(
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
        projection,
        selection,
        selectionArgs,
        sortOrder
     )?.use { cursor ->
    
        val titleColIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
        val albumColIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM)
    
        Log.d(TAG, "Query found ${cursor.count} rows")
    
        while (cursor.moveToNext()) {
            val title = cursor.getString(titleColIndex)
            val album = cursor.getString(albumColIndex)
    
            Log.d(TAG, "$title - $album")
        }
     }
  2. The code snippet above will log the title and the album values to a logging channel.

  3. Add the top-level TAG in MainActivity.kt.

     private const val TAG = "AUDIO_QUERY"
  4. Call the query() function in your ActivityResultLauncher callback.

     val permissionResultLauncher = registerForActivityResult(RequestPermission()){ isGranted ->
        if (isGranted) queryAudio()
     }
Upload the audio file

Remember the audio file that we had to download earlier? You can find its metadata in the screenshot below. We expect that our App will output the correct value for Title and Album.

1.png

For MediaStore to generate an entry, we will upload the mp3 file directly to the file system and then reboot the device so MediaStore will refresh.

  1. Boot your emulator now.
  2. In Android Studio, open the Device File Explorer from View > Tool Windows > Device File Explorer.
  3. Navigate to /storage/self/primary/Music.
  4. Right-click on Music > Upload > select the mp3 file.

2.png

  1. Cold reboot your emulator.
Run the App

Now run the app while filtering Logcat for AUDIO_QUERY, and you will see that the App correctly logs the output.

2022-01-12 16:58:58.571 8415-8415/com.example.daniwebandroidqueryaudiomediastore D/AUDIO_QUERY: Query found 1 rows
2022-01-12 16:58:58.571 8415-8415/com.example.daniwebandroidqueryaudiomediastore D/AUDIO_QUERY: Unexpected Gifts of Spring - Glint EP

MediaStore_Audio.gif

Solution Code

AndroidManifest.xml

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

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

   <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.DaniwebAndroidQueryAudioMediaStore">
       <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>

MainActivity.kt

package com.example.daniwebandroidqueryaudiomediastore

import android.Manifest.permission.READ_EXTERNAL_STORAGE
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Button
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission

private const val TAG = "AUDIO_QUERY"

class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val button = findViewById<Button>(R.id.button)

       val permissionResultLauncher = registerForActivityResult(RequestPermission()){ isGranted ->
           if (isGranted) queryAudio()
       }

       button.setOnClickListener {
           permissionResultLauncher.launch(READ_EXTERNAL_STORAGE)
       }

   }

   private fun queryAudio(){
       val projection = arrayOf(
           MediaStore.Audio.Media.TITLE,
           MediaStore.Audio.Media.ALBUM
       )

       val selection = null //not filtering out any row.
       val selectionArgs = null //this can be null because selection is also null
       val sortOrder = null //sorting order is not needed

       applicationContext.contentResolver.query(
           MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
           projection,
           selection,
           selectionArgs,
           sortOrder
       )?.use { cursor ->

           val titleColIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
           val albumColIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM)

           Log.d(TAG, "Query found ${cursor.count} rows")

           while (cursor.moveToNext()) {
               val title = cursor.getString(titleColIndex)
               val album = cursor.getString(albumColIndex)

               Log.d(TAG, "$title - $album")
           }
       }
   }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/button_startQuery"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Query Audio MediaStore</string>
   <string name="button_startQuery">Start Query</string>
</resources>
Summary

We have learned how to use the MediaStore API to query the MediaStore database. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidQueryAudioMediaStore

Android Native – How to Add Material 3 Top App Bar

Introduction

The release of Android 12 also came together with Material 3. Whether you love it or hate it, it is likely to be here to stay for a couple of years, therefore, it would be useful to know how to use it. In this tutorial, we will learn how to enable Material 3 themes and how to add the Material 3 Top App Bar into our app.

Goals

At the end of the tutorial, you would have learned:

  1. How to subclass a Material 3 theme.
  2. How to add Material 3 Top App Bar.
Tools Required
  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
  1. Basic Android.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.
Inherit Material 3 Theme

It is recommended that you apply an app-wide Material 3 theme before using any Material 3 component. Because we wanted to use the Material 3 Top App Bar, we should apply a theme such as Theme.Material3.DayNight.NoActionBar to our App.

  1. In your current project, navigate to res > values > themes.

  2. Open up both themes.xml and themes.xml (night).

  3. Inspect both files. Notice that both of their <style> elements have Theme.MaterialComponents.DayNight.DarkActionBar as the parent. This is only true on my build of Android Studio. If you are using a newer build in the future, you might see a different parent theme. The current theme is a Material 2 theme, so we will need to upgrade it to Material 3 that supports a Top App Bar.

     <style name="Theme.DaniwebAndroidMaterial3TopAppBar" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
  4. Replace the current value of the parent attribute with Theme.Material3.DayNight.NoActionBar for both XML files. This theme does not contain an ActionBar, which allows us to add a Top App Bar that conforms to Material 3 guidelines.

  5. Both of your <style> elements should look similar to this.

     <style name="Theme.DaniwebAndroidMaterial3TopAppBar" parent="Theme.Material3.DayNight.NoActionBar">
  6. But the code will not compile yet because you must add the gradle dependency for Material 3. Open up your gradle module file and replace the current material dependency of implementation 'com.google.android.material:material:1.4.0'to

     implementation' com.google.android.material:material:1.5.0-rc01'
  7. Sync the project.

  8. Run the app now and you can see that the old Action Bar is gone. The background color is also a little bit different, too.

10000201000002F600000326DF919741D4D5F1C0.png

Material 3 Top App Bar

There are many variants of the Top App Bar:

  1. Center aligned.
  2. Small.
  3. Medium.
  4. Large.

1000020100000184000001D9A011BFE668CF7C73.png

For this tutorial, we will add the center-aligned Top App Bar. Follow the steps below to add it to our app:

  1. Open up activity_main.xml in the Design view.

  2. Under the Component Tree, Right-click on ConstraintLayout > Convert view.
    10000201000001870000012397161C328A901CB1.png

  3. Select CoordinatorLayout > Apply.
    10000201000001ED000000C73E800DC420CFA532.png

  4. Switch to Code view.

  5. Add an <AppBarLayout> child to the <CoordinatorLayout>.

     <com.google.android.material.appbar.AppBarLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content">
    
     </com.google.android.material.appbar.AppBarLayout>
  6. Add a <MaterialToolBar> child to <AppBarLayout>.

     <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/topAppBar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:title="@string/page_title"
        app:titleCentered="true"
        app:menu="@menu/top_app_bar"
        app:navigationIcon="@drawable/ic_baseline_menu_24" />
  7. Add the string resource below to strings.xml.

     <string name="page_title">App Brand</string>
  8. Create a new xml resource for the menu by right-clicking on res > New > Android Resource File. Use the configuration from the screenshot below, select OK.
    100002010000040300000250CD341651C7332B4C.png

  9. Create a new drawable for the navigation icon by right-clicking on drawable > New > Vector Asset. Use the configuration from the screenshot below, and then select Next > Finish.
    1000020100000350000001CB6BC6276F2DE66846.png

  10. Remove the default Hello World! TextView.

Run the App

We are now ready to run the app, so run it now and we can see that we have a centered Top App Bar from the Material 3 library.

10000201000001940000034E7C9F50B56EFC9AD8.png

Solution Code

ic_baseline_menu_24.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:width="24dp"
   android:height="24dp"
   android:viewportWidth="24"
   android:viewportHeight="24"
   android:tint="?attr/colorControlNormal">
 <path
     android:fillColor="@android:color/white"
     android:pathData="M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z"/>
</vector>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <com.google.android.material.appbar.AppBarLayout
       android:layout_width="match_parent"
       android:layout_height="wrap_content">

       <com.google.android.material.appbar.MaterialToolbar
           android:id="@+id/topAppBar"
           android:layout_width="match_parent"
           android:layout_height="?attr/actionBarSize"
           app:title="@string/page_title"
           app:titleCentered="true"
           app:menu="@menu/top_app_bar"
           app:navigationIcon="@drawable/ic_baseline_menu_24" />

   </com.google.android.material.appbar.AppBarLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

top_app_bar.xml

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

</menu>

strings.xml

<resources>
   <string name="app_name">DaniwebAndroid Material 3 Top App Bar</string>
   <string name="page_title">App Brand</string>
</resources>

themes.xml

<resources xmlns:tools="http://schemas.android.com/tools">
   <!-- Base application theme. -->
   <style name="Theme.DaniwebAndroidMaterial3TopAppBar" parent="Theme.Material3.DayNight.NoActionBar">
       <!-- Primary brand color. -->
       <item name="colorPrimary">@color/purple_500</item>
       <item name="colorPrimaryVariant">@color/purple_700</item>
       <item name="colorOnPrimary">@color/white</item>
       <!-- Secondary brand color. -->
       <item name="colorSecondary">@color/teal_200</item>
       <item name="colorSecondaryVariant">@color/teal_700</item>
       <item name="colorOnSecondary">@color/black</item>
       <!-- Status bar color. -->
       <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
       <!-- Customize your theme here. -->
   </style>
</resources>

build.gradle

plugins {
   id 'com.android.application'
   id 'kotlin-android'
}

android {
   compileSdk 31

   defaultConfig {
       applicationId "com.example.daniwebandroidmaterial3topappbar"
       minSdk 21
       targetSdk 31
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       }
   }
   compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
   }
   kotlinOptions {
       jvmTarget = '1.8'
   }
}

dependencies {

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.0'
   implementation 'com.google.android.material:material:1.5.0-rc01'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
   testImplementation 'junit:junit:4.+'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary

We have learned how to add a Material 3 Top App Bar into our App. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidMaterial3TopAppBar

Get array of a column value in array of objects in Angular 12

Hello i have an Angular 12 project and i want to retrieve all results in artisticBehaviour (except those who are NULL or same e.g. Actor, Actor) column in Users table.

ts function that i call all users from Users table.

allArtistsSet() {
    this.userData.allArtists(this.offset).pipe(
      map((data: any) => {
        if (data.success) {
            this.allArtists = data.artistsFeed;
 this.creativenessArtists = this.allArtists.filter((data) => data.artisticBehaviour != null);
        }
      })
    ).subscribe()
  }

html

<select style="background-color: #333;color: whitesmoke;padding: 5px;font-size: small; border-radius: 5px;width: 110px;">
                <option disabled>Creative Behaviour</option>
                <option>Creativeness</option>
                <option *ngFor="let creative of creativenessArtists" value="{{creative}}">{{creative}}</option>
                </select>

Android Native – How to use WorkManager

Introduction

WorkManager is the preferred method of managing background tasks on Android. It also includes convenient extensions for RxJava3 and Kotlin Coroutines.

In this tutorial, we will learn how to use WorkManager as well as how to observe the background task with the debugging tool Background Task Inspector.

Goals

At the end of the tutorial, you would have learned:

  1. How to use WorkManager.
  2. How to use the Background Task Inspector.
Tools Required
  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
  1. Basic Android.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add the WorkManger dependency to your module build.gradle file.

     def work_version = "2.7.1"
    
     implementation "androidx.work:work-runtime-ktx:$work_version"
  3. Remove the default Hello World! TextView.

  4. Add a new Button inside ConstraintLayout.

  5. Constrain the Button to the center of ConstraintLayout.

  6. Extract the Button android:text value to strings.xml, using download_button as the resource name and Download File as the Resource value.

Your activity_main.xml should look similar to the code below.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/button_download"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Primary Classes from androidx.work

Before writing any code, we will first need to know about the primary classes of WorkManager. Most of the classes related to WorkManager can be found in the androidx.work package. The most basic classes are Worker, WorkRequest, and WorkManager.

  1. WorkManager: This class is responsible for submitting WorkRequest objects to the system. It can also cancel and read Workers. By default, the WorkManager starts itself when your App is started, so in most cases, you do not have to initialize WorkManager yourself.
  2. WorkRequest: Represents a work request, which WorkManager submits to the system. You can build custom WorkRequest objects with WorkRequest.Builder or use the two existing concrete implementations OneTimeWorkRequest or PeriodicWorkRequest. You can create OneTimeWorkRequest objects by wrapping Worker objects.
  3. Worker: This class contains code to perform the actual work for your App. Worker is abstract, so you will need to override it and implement the doWork() function.
  4. ListenableWorker.Result: The object that you must return from Work#doWork(). You can create Result objects by calling Results static functions failure(), retry(), and success().
  5. WorkInfo.State: Your work moves through different stages in the system. There are six different stages for your work: BLOCKED, CANCELLED, ENQUEUED, FAILED, RUNNING, SUCCEEDED.
Project Overview

For this tutorial, we will have our app start a fake download WorkRequest. When the user taps Download File, the background thread that executes the work request will wait for 1 second before reporting back the ListenableWorker.Result status.

We will also use the Background Task Inspector debugging facility to observe our work progress.

Override Worker

It is finally time to write some code. Our first task is to create a concrete implementation of Worker.

  1. In the same package as the MainActivity class, create a new Kotlin class called DownloadWorker.kt.

     class DownloadWorker {
    
     }
  2. Add Worker as the parent.

     class DownloadWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
    
     }
  3. Implement the doWork() function. This implementation will wait for 1 second and return a SUCCESS status.

     override fun doWork(): Result {
         Thread.sleep(1_000L)
    
         return Result.success()
     }
Retrieves the WorkManager

As stated previously, a default WorkManager is automatically initialized by our App on startup. Because our use case is quite simple and does not require a custom WorkManager, we can just use the default WorkManager instance using the convenient static function WorkManger#getInstance().

  1. Inside MainActivity.kt#onCreate(), append the line of code below.

     val workManager = WorkManager.getInstance(applicationContext)
  2. Create the a OneTimeWorkRequest object by appending the line of code below to onCreate(). You must pass in the Java class object to the from() function.

     val oneTimeDownloadWorkRequest = OneTimeWorkRequest.from(DownloadWorker::class.java)
  3. Append the line of code below to onCreate() retrieve the Button object.

     val button = findViewById<Button>(R.id.button)
  4. Bind the Button click to the work request in onCreate().

     button.setOnClickListener {
        workManager.enqueue(oneTimeDownloadWorkRequest)
     }
Background Task Inspector

The Background Task Inspector is a special debugging facility available when your WorkManager runs at Android API level 26 or above. To see how it works, follow the steps below:

  1. In Android Studio, go to View > Tool Windows > App Inspection.
  2. This tool also contains the Database Inspector, so ignore that tab for now. When your App is started, switch to the Background Task Inspector.
    1.png
  3. Run the App.
  4. Taps Download File button in your App and observe the background job being executed.

WorkManager.gif

Solution Code

DownloadWorker.kt

package com.example.daniwebandroidworkmanager

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters

class DownloadWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {

   override fun doWork(): Result {
       Thread.sleep(1_000L)

       return Result.success()
   }

}

MainActivity.kt

package com.example.daniwebandroidworkmanager

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val workManager = WorkManager.getInstance(applicationContext)
       val oneTimeDownloadWorkRequest = OneTimeWorkRequest.from(DownloadWorker::class.java)

       val button = findViewById<Button>(R.id.button)

       button.setOnClickListener {
           val operation = workManager.enqueue(oneTimeDownloadWorkRequest)
       }
   }
}

strings.xml

<resources>
   <string name="app_name">Daniweb Android WorkManager</string>
   <string name="button_download">Download File</string>
</resources>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/button_download"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Summary

We have learned how to use WorkManager and the Background Task Inspector. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidWorkManager

Android Native – Load Images with Coil

Introduction

Coil is a popular image loading library for Kotlin-first Android projects. It is lightweight, fast and is super easy to use.

In this tutorial, we will learn how to use Coil to load images into our project.

Goals

At the end of the tutorial, you would have learned:

  1. How to load images with Coil.
Tools Required
  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
  1. Basic Android.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Remove the Default Hello World! TextView.

  3. Add a new ImageView inside ConstraintLayout.

     <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:srcCompat="@tools:sample/avatars" />
  4. Add a new Button below ImageView with the code below.

     <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/load_image"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />
  5. Add the <string> elements below to your strings.xml file.

     <string name="load_image">Random Image</string>
     <string name="circle">https://github.com/dmitrilc/DaniwebAndroidNativeCoil/raw/main/app/src/main/res/drawable/circle.png</string>
     <string name="rect">https://github.com/dmitrilc/DaniwebAndroidNativeCoil/raw/main/app/src/main/res/drawable/rect.png</string>
     <string name="triangle">https://github.com/dmitrilc/DaniwebAndroidNativeCoil/raw/main/app/src/main/res/drawable/triangle.png</string>
  6. The dependency to the Coil library to your module gradle.build file.

     implementation 'io.coil-kt:coil:1.4.0'
  7. Inside your AndroidManifest.xml, Add the android.permission.INTERNET permission before <application>, but after <manifest>.

     <uses-permission android:name="android.permission.INTERNET" />
Coil Packages

The Coil library includes a handful of artifacts. For the two base artifacts, certain variants contain a singleton ImageLoader instance and (Kotlin) extension functions for ImageView.

  1. io.coil-kt:coil-base: base artifact
  2. io.coil-kt:coil: base artifact and include singleton ImageLoader and ImageView extension functions.

Support for Jetpack Compose:

  1. io.coil-kt:coil-compose-base: supports Jetpack Compose.
  2. io.coil-kt:coil-compose: supports Jetpack Compose and includes singleton ImageLoader and ImageView extension functions.

Regarding the versions with the singleton ImageLoader, the library even went as far as to say that you should use the version without the singleton ImageLoader instead if you plan to inject the ImageLoader instance using dependency injection. If you are instantiating a custom ImageLoader instance and having a global singleton instance as well, then you might end up with multiple instances of ImageLoader at runtime.

For our tutorial, we will use the version with the singleton ImageLoader and ImageView extension functions.

Coil also includes artifacts for supporting gif, svg, and video formats.

  1. io.coil-kt:coil-gif: support for gif.
  2. io.coil-kt:coil-svg: support for svg.
  3. io.coil-kt:coil-video: support for video formats.
Primary Classes of Coil

Coil has two primary classes, ImageLoader and ImageRequest.

  1. ImageLoader: the service class that can submit ImageRequest objects.
  2. ImageRequest: a data object that provides information for a loading action, such as where to load images (with its data property) or the target ImageView to load the Image to (with its target property).

It is good to know about these two classes, but if you are using the io.coil-kt:coil artifact, then you barely have to write any boilerplate code at all. Loading an image is as simple as calling one of the load() extension functions on an ImageView reference.

Load images with ImageView extension functions

The simplest way to get an image is to use the ImageView extension functions.

  1. In MainActivity#onCreate(), get the Button reference.

     val button = findViewById<Button>(R.id.button)
  2. Then, get the ImageView reference.

     val imageView = findViewById<ImageView>(R.id.imageView)
  3. Finally, bind the Button to a load() action. The code below will randomly load one of three images from this projects github repository.

     button.setOnClickListener {
        when(Random.nextInt(0, 3)){
            0 -> imageView.load(getString(R.string.rect))
            1 -> imageView.load(getString(R.string.triangle))
            2 -> imageView.load(getString(R.string.circle))
        }
     }
Load images with ImageLoader

This is the longer route and is unnecessary for our project, but I will still show it as reference. In onCreate(), also add the multi-line comment below.

/** Long way to load an image
val imageLoader = applicationContext.imageLoader
val request = ImageRequest.Builder(applicationContext)
   .data(getString(R.string.rect))
   .target(imageView)
   .build()

imageLoader.enqueue(request)
**/
Run the App

We are now ready to run our App. In the gif below, I have also included the Profiler output to demonstrate how resources were used when loading the image.

Coil.gif

Solution Code

MainActivity.kt

package com.example.daniwebandroidnativecoil

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import coil.Coil
import coil.imageLoader
import coil.load
import coil.request.ImageRequest
import coil.util.CoilUtils
import okhttp3.OkHttpClient
import kotlin.random.Random

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val button = findViewById<Button>(R.id.button)
       val imageView = findViewById<ImageView>(R.id.imageView)

       button.setOnClickListener {
           when(Random.nextInt(0, 3)){
               0 -> imageView.load(getString(R.string.rect))
               1 -> imageView.load(getString(R.string.triangle))
               2 -> imageView.load(getString(R.string.circle))
           }
       }

       /** Long way to load an image
       val imageLoader = applicationContext.imageLoader
       val request = ImageRequest.Builder(applicationContext)
           .data(getString(R.string.rect))
           .target(imageView)
           .build()

       imageLoader.enqueue(request)
       **/
   }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <ImageView
       android:id="@+id/imageView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       tools:srcCompat="@tools:sample/avatars" />

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/load_image"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/imageView" />
</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Android Native Coil</string>
   <string name="load_image">Random Image</string>
   <string name="circle">https://github.com/dmitrilc/DaniwebAndroidNativeCoil/raw/main/app/src/main/res/drawable/circle.png</string>
   <string name="rect">https://github.com/dmitrilc/DaniwebAndroidNativeCoil/raw/main/app/src/main/res/drawable/rect.png</string>
   <string name="triangle">https://github.com/dmitrilc/DaniwebAndroidNativeCoil/raw/main/app/src/main/res/drawable/triangle.png</string>
</resources>

build.gradle

plugins {
   id 'com.android.application'
   id 'kotlin-android'
}

android {
   compileSdk 31

   defaultConfig {
       applicationId "com.example.daniwebandroidnativecoil"
       minSdk 21
       targetSdk 31
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       }
   }
   compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
   }
   kotlinOptions {
       jvmTarget = '1.8'
   }
}

dependencies {

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.0'
   implementation 'com.google.android.material:material:1.4.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
   implementation 'io.coil-kt:coil:1.4.0'
   testImplementation 'junit:junit:4.+'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary

We have learned how to use Coil to load images. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidNativeCoil

Software for creating image

I've been code for android/ios using flutter more than a year. Now I would like to learn new things by using software to create image instead of just getting icons from google material design (just like image below).

Any recommended ? Thanks.

104648412-d93f8f00-5691-11eb-94e9-d1ecedd7c9f8.png

Android Native – Take a Picture with ActivityResult API

Introduction

In this tutorial, we will learn how to capture a picture with the new ActivityResult APIs(androidx.activity.result), which replace the startActivityForResult() and onActivityResult() APIs(from Activity class).

Additionally, we will also learn about the ContentProvider FileProvider, which we will have to set up to save our images.

Goal

At the end of the tutorial, you would have learned:

  1. How to use ActivityResult APIs to launch another Activity(default Camera) from your App.
  2. How to set up and use FileProvider.
Tools Required
  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
  1. Intermediate Android.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Remove the default Hello World! TextView.

  3. Add a new Button inside ConstraintView using the code below.

     <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button_takePicture"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
  4. Add these string resources in strings.xml. You should modify the authorities string value if your domain name is different.

     <string name="button_takePicture">Take Picture</string>
     <string name="temp_images_dir">temp_images</string>
     <string name="temp_image">temp_image.jpg</string>
     <string name="authorities">com.example.fileprovider</string>
  5. Add a new ImageView under the Button using the code below.

     <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button"
        tools:srcCompat="@tools:sample/avatars" />
  6. Your activity_main.xml should look like the screenshot below.

1.png

Project Overview

Firstly, let us quickly go over the starter project.

  1. The project mostly has only two elements, a Button and an ImageView.
  2. The Button is centered inside the parent ConstraintLayout.
  3. The ImageView does not contain any Drawable resource at runtime. The avatar image that you see right now only shows up during development.
  4. The ImageView has 8dp margins at the top and the bottom and it is constrained to sit below the Button.

When the project is completed, our App will be able to perform the following actions:

  1. When the Button is clicked, our App will launch the default Camera app.
  2. After a picture is taken with the Camera app, Android will automatically return our App to the foreground.
  3. The newly taken picture will be displayed inside the ImageView element.
  4. Anytime we take a new picture, the ImageView will refresh its current Drawable.
FileProvider Setup

Before we can implement the ActivityResult API, we need to agree on a way to save the taken pictures first. For this tutorial, we will use the FileProvider API, which encapsulates File URIs as Content URIs(android.net.Uri). launch() functions from ActivityResultLauncher specifically require Uri objects.

To set up FileProvider for our project, there are a couple of steps that we need to follow:

  1. Define a FileProvider in the manifest (inside the <application> element, but outside of the <activity> element). We need android:grantUriPermission to be true here because the Camera App will need temporary access to Uri objects internal to our App via the FileProvider. If you are not sure what these attributes do to <provider>, you can check out the docs here.

     <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="@string/authorities"
        android:exported="false"
        android:grantUriPermissions="true">
     </provider>
  2. FileProvider can only generate Uri objects from files that you specified beforehand, so we need to pass the <meta-data> element below into <provider> to let the FileProvider know which files it can generate Uri objects for.

     <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
  3. Notice the @xml/file_paths resource. We will have to create it because it does not exist yet. Right-click on the res directory > New > Android Resource File. Create the new resource file with the parameters matching the screenshot below.
    2.png

  4. Add the xml content below into file_paths.xml.

     <paths>
        <files-path name="@strings/temp_images" path="."/>
     </paths>
Generate the Uri

Next, we will need to generate the Uri object.

  1. Inside the MainAcitivity class, copy and paste the function below.

     private fun initTempUri(): Uri {
    
     }
  2. Add the code below into initTempUri(). This code snippet creates an abstract file path to the temporary directory that will house the temporary file later.

     //gets the temp_images dir
     val tempImagesDir = File(
        applicationContext.filesDir, //this function gets the external cache dir
        getString(R.string.temp_images_dir)) //gets the directory for the temporary images dir
  3. Because this directory does not yet exist, we must create it in the file system (our Apps private storage area) with the line of the code below.

     tempImagesDir.mkdir() //Create the temp_images dir
  4. Now we need to create an abstract file path for the temporary image file.

     //Creates the temp_image.jpg file
     val tempImage = File(
        tempImagesDir, //prefix the new abstract path with the temporary images dir path
        getString(R.string.temp_image)) //gets the abstract temp_image file name
  5. Finally, we create and return the Uri object from the File object with FileProvider#getUriForFile().

     //Returns the Uri object to be used with ActivityResultLauncher
     return FileProvider.getUriForFile(
        applicationContext,
        getString(R.string.authorities),
        tempImage)
ActivityResult API

To be able to work with ActivityResult APIs, it is crucial to understand the classes below:

  1. ActivityResultCaller: interface with abstract functions registerForActivityResult(). AppCompatActivity (as well many other classes) implements this interface, so we are able to call registerForActivityResult() inside our MainActivity.
  2. ActivityResultContract: the first argument for registerForActivityResult(). There are pre-made implementations of this interface, so we do not have to implement it in this tutorial.
  3. ActivityResultContracts: a collection of pre-made ActivityResultContract implementations.
  4. ActivityResultContracts.TakePicture: the premade ActivityResultContract that we use in this tutorial to take a picture.
  5. ActivityResultCallback: A functional interface and the second argument for registerForActivityResult(). The return type of its SAM is dependent on the ActivityResultContract passed into registerForActivityResult() in the first argument. For the TakePicture object used in this tutorial, ActivityResultCallback SAM returns a Boolean.
  6. ActivityResultLauncher: the return value of registerForActivityResult(), which we will call launch() on and pass in our Uri object.
Use ActivityResult APIs to launch another Activity

Now that we are familiar with the classes in androidx.activity.result.contract, we can finally use them in our code.

  1. Add the function below into the MainActivity class.

     private fun registerTakePictureLauncher(path: Uri) {
    
     }
  2. Get references to the Button and ImageView objects.

     val button = findViewById<Button>(R.id.button) //gets the Button object
     val imageView = findViewById<ImageView>(R.id.imageView) //gets the ImageView object
  3. Then, we call registerForActivityResult(), passing in the premade ActivityResultContracts.TakePicture and setting the ImageView URI. There a few different ImageView#setX() functions, but setImageURI() was chosen here for simplicity because we already have an Uri object. You should also test the image dimensions on older versions of Android; that topic is out of scope for this tutorial, so I picked the simplest method calls to use.

     //Creates the ActivityResultLauncher
     val resultLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()){
        imageView.setImageURI(null) //rough handling of image changes. Real code need to handle different API levels.
        imageView.setImageURI(path)
     }
  4. Finally, we bind the Button click actions to launch the Camera activity.

     //Launches the camera when button is pressed.
     button.setOnClickListener {
        resultLauncher.launch(path) //launches the activity here
     }
  5. Inside onCreate(), call the two functions that we have created like below.

     //initialize temp image directory on first launch
     val tempImageUri = initTempUri()
    
     registerTakePictureLauncher(tempImageUri) //Binds button to launch camera activity
Run the App

Launch the App to test if it works. When you are inside the simulated Camera app, hold Alt + WASD/Mouse to move around.

The app behavior should be similar to the animation below.

Camera.gif

Solution Code

AndroidManifest.xml

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

   <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.DaniwebAndroidCameraPictureIntent">
       <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>
       <provider
           android:name="androidx.core.content.FileProvider"
           android:authorities="@string/authorities"
           android:exported="false"
           android:grantUriPermissions="true">
           <meta-data
               android:name="android.support.FILE_PROVIDER_PATHS"
               android:resource="@xml/file_paths" />
       </provider>
   </application>

</manifest>

MainActivity.kt

package com.example.daniwebandroidcamerapictureintent

import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import java.io.File

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       //initialize temp image directory on first launch
       val tempImageUri = initTempUri()

       registerTakePictureLauncher(tempImageUri) //Binds button to launch camera activity
   }

   private fun initTempUri(): Uri {
       //gets the temp_images dir
       val tempImagesDir = File(
           applicationContext.filesDir, //this function gets the external cache dir
           getString(R.string.temp_images_dir)) //gets the directory for the temporary images dir

       tempImagesDir.mkdir() //Create the temp_images dir

       //Creates the temp_image.jpg file
       val tempImage = File(
           tempImagesDir, //prefix the new abstract path with the temporary images dir path
           getString(R.string.temp_image)) //gets the abstract temp_image file name

       //Returns the Uri object to be used with ActivityResultLauncher
       return FileProvider.getUriForFile(
           applicationContext,
           getString(R.string.authorities),
           tempImage)
   }

   private fun registerTakePictureLauncher(path: Uri) {
       val button = findViewById<Button>(R.id.button) //gets the Button object
       val imageView = findViewById<ImageView>(R.id.imageView) //gets the ImageView object

       //Creates the ActivityResultLauncher
       val resultLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()){
           imageView.setImageURI(null) //rough handling of image changes. Real code need to handle different API levels.
           imageView.setImageURI(path)
       }

       //Launches the camera when button is pressed.
       button.setOnClickListener {
           resultLauncher.launch(path) //launches the activity here
       }
   }

}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/button_takePicture"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <ImageView
       android:id="@+id/imageView"
       android:layout_width="wrap_content"
       android:layout_height="0dp"
       android:layout_marginTop="8dp"
       android:layout_marginBottom="8dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/button"
       tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>
strings.xml
<resources>
   <string name="app_name">Daniweb Android Camera Picture Intent</string>
   <string name="button_takePicture">Take Picture</string>
   <string name="temp_images_dir">temp_images</string>
   <string name="temp_image">temp_image.jpg</string>
   <string name="authorities">com.example.fileprovider</string>
</resources>
file_paths.xml
<paths>
   <files-path name="@strings/temp_images" path="."/>
</paths>
Summary

We have learned how to capture an image with the default Camera and return back to our Activity using ActivityResult APIs as well as how to use FileProvider.

The full project code can be found here https://github.com/dmitrilc/DaniwebAndroidCameraPictureIntent

Android Native – How to use Navigation Component

Introduction

Navigation component is an abstraction on top of FragmentManager, which simplifies navigation between fragments. In this tutorial, we will learn how to use the Navigation component in our App.

Goal

At the end of the tutorial, you would have learned:

  1. How to use the Navigation Component.
Tools Required
  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
  1. Basic Android.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Remove the default Hello World! TextView.

  3. Add the two string resources below into strings.xml.

     <string name="hello_blank_fragment">Hello blank fragment</string>
     <string name="second_screen">2nd Screen</string>
  4. Add dependencies below to your Module gradle file.

     def nav_version = "2.3.5"
    
     // Kotlin
     implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
     implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
Navigation Component Concept Overview

Before we can start using the Navigation component, we will need to understand the basic concepts of this library.

  1. Navigation Graph: this is an XML file that contains the navigation logics for your Destinations. Android Studio includes a powerful GUI editor to make it easy to visualize your Apps navigation flow.
    10000201000006280000029B667BE17906B2A331.png
  2. Navigation Host: this is an empty activity that houses a NavHostFragment.
  3. NavHostFragment: an object whose primary job is swapping out destinations using its NavController.
  4. NavController: an object with navigate() functions that you can call to direct user navigation.
  5. Destination: where the user navigates to.
  6. Home Destination: the first screen that the user sees.
Create the Nav graph

Now that we are somewhat familiar with the basic concepts, the first thing that we will need to do is to create the Navigation Graph XML file by following the steps below.

  1. Right-click on res > New > Android Resource File.
  2. Generate the nav_graph.xml file based on the settings in the below screenshot. This will place the file under the navigation directory as well as letting Android Studio know that this file can be opened with the Navigation Editor.
    10000201000003FC00000220958C87BFA99664B9.png
  3. Open nav_graph.xml in the Design surface. Notice that there is currently zero Host under the Hosts panel.

100002010000011C000001CF5BAC800E9F0FCB11.png

Designate an Activity as the Navigation Host

We need a Navigation Host to house the NavHostFragment. Follow the steps below to add a Navigation Host:

  1. Open activity_main.xml.

  2. Palette > Container > (select) NavHostFragment.
    100002010000017D000000A5EE21CAEF5E0E7ED1.png

  3. Drag NavHostFragment into the Component Tree.
    100002010000021200000074DFA962A23D54C486.png

  4. Go back to nav_graph.xml in the Design surface, we will now see that acitivty_main now shows up as a Host. This means that the activity_main Navigation Host is associated with this navigation graph (nav_graph.xml).
    10000201000001410000008DB3BDBE45FF5CDA01.png

  5. Now open activity_main.xml in Code view. The app:navGraph attribute that you see here determines that association.

     app:navGraph="@navigation/nav_graph"
Home Destination

Next, we need to create a new destination(Fragment) and designate it as the Home Destination.

  1. Open nav_graph.xml in the Surface view.

  2. Select the New Destination icon > Create new destination.
    10000201000001F8000000FACB9D96E1C75D4CD3.png

  3. We only need a very simple Fragment, so select Fragment (Blank) > Next.
    10000201000001140000011F19E301EDB89A63AB.png

  4. Use BlankFragment as the Fragment Name.

  5. Use fragment_blank as the Fragment Layout Name > Finish.
    10000201000000AB0000009E5DEFB4BC2DC36B0F.png

  6. Designate fragment_blank as the Home Destination by selecting it and then click on the house icon.
    10000201000000F10000006678271276887EAEA0.png

  7. While we are at it, let us modify fragment_blank.xml a little bit more for this tutorial. Remove the default TextView from this fragment.

  8. Convert the Fragment FrameLayout to ConstraintLayout**.
    10000201000001D00000004D2485626060142E12.png

  9. Add a new Button inside ConstraintView using the code below. We will use this Button to navigate to another screen later.

     <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/second_screen"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
Add another Destination

Next, we will need to add another destination(Fragment) to navigate to. Repeat the steps 1-5 in the previous section, but suffix this fragments Fragment Name and Fragment Layout Name with a number 2.

Connect the Destinations

Now that we have both destinations, we can start connecting them with actions.

  1. Connect the two fragments by dragging the circle on the right side of blankFragment to blankFragment2.
    10000201000003080000024013900087C3F5632A.png
  2. Alternatively, you can also use the Add Action button to link the two destinations.
    100002010000026B0000030F97CDD7A732D58D8E.png
  3. After connecting the two destinations, you should see something similar to the screenshot below. The Navigation Editor becomes a powerful tool to visualize your Application flow especially when you have a lot of destinations.
    10000201000003F2000002D490D9ED886E8FB6A3.png
Navigate to Destinations

To navigate to another destination, we will need to obtain the reference of the NavController object.

  1. Inside BlankFragment.kt, override the onViewCreated() callback.

     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
    
     }
  2. Inside onViewCreated(), get a reference to the NavController object with findNavController(). This function comes from the navigation-fragment-ktx dependency that we added in the beginning.

     val navController = findNavController() //gets the navController
  3. Get a reference to the Button.

     val button = view.findViewById<Button>(R.id.button) //finds the Button
  4. Bind the Button onClickListener to a Navigation action with the code below. Note that we are using the android:id of the <action> element located inside nav_graph.xml here.

     button.setOnClickListener {
        //navigate using the Action ID, not fragment ID
        navController.navigate(R.id.action_blankFragment_to_blankFragment2)
     }
Run the app

We are now ready to run the App. Try clicking on the Button to navigate to the next destination in the flow and then back.

10000022000001D7000003785FCA2C5F17C99608.gif

Solution Code

build.gradle

plugins {
   id 'com.android.application'
   id 'kotlin-android'
}

android {
   compileSdk 31

   defaultConfig {
       applicationId "com.example.daniwebnavigationcomponentsbasics"
       minSdk 21
       targetSdk 31
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       }
   }
   compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
   }
   kotlinOptions {
       jvmTarget = '1.8'
   }
}

dependencies {
   implementation 'androidx.legacy:legacy-support-v4:1.0.0'
   def nav_version = "2.3.5"

   // Kotlin
   implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
   implementation("androidx.navigation:navigation-ui-ktx:$nav_version")

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.0'
   implementation 'com.google.android.material:material:1.4.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
   testImplementation 'junit:junit:4.+'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

strings.xml

<resources>
   <string name="app_name">Daniweb Navigation Components Basics</string>
   <string name="hello_blank_fragment">Hello blank fragment</string>
   <string name="second_screen">2nd Screen</string>
</resources>

nav_graph.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/nav_graph"
   app:startDestination="@id/blankFragment">

   <fragment
       android:id="@+id/blankFragment"
       android:name="com.example.daniwebnavigationcomponentsbasics.BlankFragment"
       android:label="fragment_blank"
       tools:layout="@layout/fragment_blank" >
       <action
           android:id="@+id/action_blankFragment_to_blankFragment2"
           app:destination="@id/blankFragment2" />
   </fragment>
   <fragment
       android:id="@+id/blankFragment2"
       android:name="com.example.daniwebnavigationcomponentsbasics.BlankFragment2"
       android:label="fragment_blank2"
       tools:layout="@layout/fragment_blank2" />
</navigation>

fragment_blank.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/frameLayout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".BlankFragment" >

   <Button
       android:id="@+id/button"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/second_screen"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

fragment_blank2.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".BlankFragment2">

   <!-- TODO: Update blank fragment layout -->
   <TextView
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:text="@string/hello_blank_fragment" />

</FrameLayout>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/fragmentContainerView"
       android:name="androidx.navigation.fragment.NavHostFragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       app:defaultNavHost="true"
       app:navGraph="@navigation/nav_graph"
       />

</androidx.constraintlayout.widget.ConstraintLayout>

BlankFragment.kt

package com.example.daniwebnavigationcomponentsbasics

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.navigation.fragment.findNavController

// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

/**
* A simple [Fragment] subclass.
* Use the [BlankFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class BlankFragment : Fragment() {
   // TODO: Rename and change types of parameters
   private var param1: String? = null
   private var param2: String? = null

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       arguments?.let {
           param1 = it.getString(ARG_PARAM1)
           param2 = it.getString(ARG_PARAM2)
       }
   }

   override fun onCreateView(
       inflater: LayoutInflater, container: ViewGroup?,
       savedInstanceState: Bundle?
   ): View? {
       // Inflate the layout for this fragment
       return inflater.inflate(R.layout.fragment_blank, container, false)
   }

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

       val navController = findNavController() //gets the navController

       val button = view.findViewById<Button>(R.id.button) //finds the Button

       button.setOnClickListener {
           //navigate using the Action ID, not fragment ID
           navController.navigate(R.id.action_blankFragment_to_blankFragment2)
       }
   }

   companion object {
       /**
        * Use this factory method to create a new instance of
        * this fragment using the provided parameters.
        *
        * @param param1 Parameter 1.
        * @param param2 Parameter 2.
        * @return A new instance of fragment BlankFragment.
        */
       // TODO: Rename and change types and number of parameters
       @JvmStatic
       fun newInstance(param1: String, param2: String) =
           BlankFragment().apply {
               arguments = Bundle().apply {
                   putString(ARG_PARAM1, param1)
                   putString(ARG_PARAM2, param2)
               }
           }
   }
}
Summary

We have learned how to use the Navigation Component.

The full project code can be found here https://github.com/dmitrilc/DaniwebNavigationComponentsBasics

Android Native – How to load Samsung Emulator Skins

Introduction

Based on the latest report from Appbrain, Samsung has the highest market share(>35%) among all of the Android OEMs, therefore it is important that your apps are tested against Samsung phones.

In this tutorial, we will learn how to load Samsung Emulator skins onto our emulator devices.

Goals

At the end of the tutorial, you would have learned:

  1. How to load Samsung Emulator skins onto Android emulators with Android Studio.
  2. How custom AVD skins work.
Tools Required
  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
  1. Basic Android.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.
AVD Skins

Before learning how to add Samsung Emulator skins to our emulators, first, let us dig a little bit deeper into custom AVD (Android Virtual Device) skins.

For each emulator managed by the AVD Manager, there is an advanced option called Custom skin definition, which allows you to load a skin that is different from stock Android.

10000201000001C10000002F981313D99AB37890.png

You can either get these skins directly from the OEMs or create them yourselves. Unfortunately, not all Android OEMs provide them to developers, but Samsung is one of the few OEMs that provides a collection of pre-made skins for their devices.

Custom Skin only applies to the emulator frame

Even though we can make our stock emulators look and feel like real Samsung devices, this approach does not replicate the functionalities of the Launcher and other System applications on Samsung phones, so you still need to test with real devices or cloud testing farms for more accurate results.

The emulator skin can only emulate volume buttons and camera/speaker/microphone cutouts. Refer to the picture below if you need further clarification.

10000201000001D0000003FBABA9DB1D4F1CE307.png

Anatomy of AVD Skins

When you are loading a skin, you are simply loading a directory containing skin configuration files and graphic assets. A skin directory contains the files below:

  1. Configuration file named layout: this is the main configuration file, which defines the appearance of the skin. The format of this file looks similar to JSON, but its actual format must follow the rules described in ANDROID-CONFIG-FILES.TXT. Its supported key-value pairs are documented in ANDROID-SKIN-FILES.TXT.
  2. The image files for custom elements of the skin.
  3. Configuration file named hardware.ini: optional file containing custom hardware configurations for the emulator.

Besides OEM device skins that we can download from the internet, Android Studio already includes pre-made skins for us to inspect. On Windows, by default, they are located at the locations below:

  1. %USERPROFILE%\AppData\Local\Android\Sdk
  2. %USERPROFILE%\AppData\Local\Android\Sdk\platforms\android-31\skins

You will need to modify the platform level in the path above if you do not have android-31 installed.

I have opened the HVGA skin (under android-31/skins), which includes the layout, hardware.ini, and graphical assets in the picture below.

100002010000056E000001879C4FECF61A828BCD.png

Get Emulator Skins from Samsung

For this tutorial, we will learn how to load the emulator skin of a Galaxy S21+. To download this skin, follow the steps below:

  1. Go to this link to access all Galaxy S skins. A free Samsung account is required to access this page.
    100002010000008C00000108AA334F120350F879.png
  2. Scroll download until you find Galaxy S21+, and then click DOWNLOAD SKIN.
    100002010000026F00000108E31CC34920322869.png
  3. Find the downloaded Galaxy_S21_Plus.zip file and extract the content.
    10000201000000B50000002DF51D8ED3CE5F6441.png
Inspecting the Configuration Files

Using what we know so far about the layout configuration file, we will now try to understand the layout file included in the Galaxy S21+ skin.

  1. Inside the extracted Galaxy_S21_Plus directory, open up the layout file with a text editor. Under the parts/portrait section, we can easily see which configuration is referring to which graphic asset.
    100002010000032A000001BB64D7045CA8426C17.png
  2. There are three different colors of the Galaxy S21+, and the Black color is used by default, so thats why device_Port_PhantomBlack.png is used in the configuration file. The Silver and Violet backgrounds are not used, but you are allowed to modify the configuration file as you wish.
  3. The foreground mask is applied on top of the background to give the skin the correct curves as well as the camera cutout.
  4. volume_up, volume_down, and power assets emulate the volume rockers and the power button, including their X and Y coordinates.
  5. Unfortunately, this skin from Samsung does not include the hardware.ini file, so we will have to apply the emulator hardware configurations by ourselves later.
Create the Galaxy S21+ Emulator

For the skin to apply properly, we will have to create an emulator with the same dimensions as the real device. The resolution of the Galaxy S21+ is 1080x2400, so our emulator must use the same resolution as well.

  1. In Android Studio, Open the AVD Manager from Tools > AVD Manager.
  2. At the bottom left, select Create Virtual Device.
    10000201000000DE00000031B4C232784B6D0582.png
  3. On the next screen, select New Hardware Profile.
    10000201000000C500000023429952E429D5ACCC.png
  4. For the Device Name, use Mock Galaxy S21_Plus.
  5. For the Device Type, use Phone/Tablet.
  6. The screen size must be 6.7.
  7. The resolution would be 1080x2400.
  8. Leave Input, Supported device states, Camera, and Sensors as default.
    100002010000045A000002563EB0FED8D358BECF.png
  9. Scroll all the way to the end where you will find the Default skin option. Select the 3 dots icon and then select the Galaxy_S21_Plus directory that you have extracted earlier.
    100002010000029800000032C1D80E02F86B786F.png
  10. You will now see the Galaxy_S21_Plus skin set as the Default skin.
    100002010000028800000025AA5AE80B726C2BE0.png
  11. Click Finish. The window will close and bring us back to the previous screen.
  12. With the Mock Galaxy S21_Plus selected, hit Next.
    1000020100000421000002DD712DE717F22F21CE.png
  13. Now select your System Image of choice and select Next. I am using the x86 API 31 Image, but you might have to select a different Image depending on your own system (ARM-based systems such as Qualcomm/Raspberry Pi/Apple ARM should use ARM images instead of x86).
    1000020100000443000002DEF424843121015C74.png
  14. On the last screen, make sure that Enable Device Frame is selected before hitting Finish.
    10000201000004C40000027D4328EE5DB2D3602C.png
  15. Back to AVD Manager, press the green Play button to start your emulator.
    1000020100000465000000514540AAAA88DC6474.png

Your system might take a couple of minutes to start the emulator for the first time, but that should not affect the emulator skin from loading. You should see your emulator with a similar interface to the picture below.

1000020100000210000003FAAAB66CC5979685DF.png

If we compare our emulator skin with the real stock images, then we can see that the antennas, camera cutout, volume rockers, and the power button are all at the right places.

1000020100000525000003D346EDC6C9964B249E.png

Summary

We have learned how to load custom skins on Android emulators.

For this particular S21+ skin, I think the real benefits are to see where the camera cutout is as well as the volume/power rockers, especially if your app focuses on flagship galaxy devices. Maybe it will help you design your game controls better so they are not accidentally placed under the camera cutout; or maybe now that you know where the volume rockers and power buttons are, you would be able to avoid forcing your users to hold the device in an awkward position when using your app.

Android Native – Adapt code to displays with a Notch

Introduction

Regardless of whether you like notches on your devices or not, they exist, so as developers, we will have to adapt our Apps to work with them. In this tutorial, we will learn how to adapt our code to work with notches.

The device used in this example is the Pixel 3 XL because Android Studio already provides an AVD skin for this device.

Goals

At the end of the tutorial, you would have learned:

  1. How to adapt your App to work with display cutouts.
Tools Required
  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
  1. Basic Android.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. In Android Studio, open the AVD Manager by going to Tools > AVD Manager.

  3. Select Create Virtual Device from the bottom left.
    10000201000000DB0000002E64217D460D05F7A1.png

  4. In the Phone Category, select the Pixel 3 XL, then hit Next.
    10000201000003EA00000274B5BEBC38EDF36B64.png

  5. Select the Android S Image with API Level 32 (x86 or ARM depends on your PC), and hit Next.
    1.png

  6. On the last screen, verify that Enable Device Frame is checked, and then hit Finish.
    2.png

  7. Create another AVD with the exact same configuration, but without the Device Frame.

  8. I was unable to find any documentation about whether the emulator is aware of the display cutout, but based on testing, the emulator is, in fact, aware of cutout areas from emulator skins, without requiring the developer to turn on Display cutout emulation in Developer settings. In the picture below, you can clearly see how the notification bar renders its content in a way that does not obstruct the emulator skin.
    100002010000033700000364830099FE6A5E5836.png

  9. In themes.xml, replace the current theme of

     parent="Theme.MaterialComponents.DayNight.DarkActionBar"

    with

     parent="Theme.MaterialComponents.DayNight.NoActionBar"
  10. In strings.xml, add the text element below. The string is A-z to make it easy to check the overlay behavior later.

     <string name="super_long_text">AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz</string>
  11. Replace your activity_main.xml with the xml content below. The modified layout removes the bottom constraint of the TextView as well as setting its text value to the string resource declared in the previous step.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/super_long_text"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
     </androidx.constraintlayout.widget.ConstraintLayout>
  12. Add the below style element into your main themes.xml file (outside of the current <style>, but still inside <resources> pairs). Make sure to modify the parent attribute because your base theme will have a different name compared to mine.

     <style name="AppTheme" parent="Theme.DaniwebCutoutArea" />
  13. Create a new resource file for Android API 27 (I will explain why later). Right-click on themes > New > Values Resource File. Add the qualifier and name the file themes.
    100002010000045500000272BA81D98D2891FCD7.png

  14. Paste the content below into the new resource file (themes.xml (v27)) to override your base theme. Modify the parent attribute if necessary.

     <resources xmlns:tools="http://schemas.android.com/tools">
        <!-- Base application theme. -->
        <style name="AppTheme" parent="Theme.DaniwebCutoutArea">
            <!-- Customize your theme here. -->
        </style>
     </resources>
  15. In AndroidManifest.xml, replace the current android:theme attribute with the attribute below.

     android:theme="@style/AppTheme"
  16. Launch the app on both devices simultaneously by going to Run > Select Device >Select Multiple Devices > Check your two devices > OK > click the Play button to run the app on both AVDs.
    100002010000018A00000069975C34A60BBD86D1.png

You should now see something similar to the screenshot with the app running on both AVDs side-by-side.

10000201000003F0000000A2C4DEC1D7B1D24016.png

Fullscreen Display

Display cutouts tend to present more problems to fullscreen Apps than non-fullscreen Apps. When your app needs to utilize the entire screen, by default, devices with a notch will refuse to allow your App to render inside the cutout area.

To understand this, first, let us attempt to make our App display in full screen. At the minimum, on Systems without a notch, you must do at least one thing for your app to display in fullscreen:

  1. Hide the System bars.

To do this, add the lines of code below into the onCreate() callback in MainActivity.kt, after the setContentView() call.

//Hide the system bars
val windowInsetsController: WindowInsetsControllerCompat? = ViewCompat.getWindowInsetsController(window.decorView) //gets WindowInsetsController
windowInsetsController?.hide(WindowInsetsCompat.Type.systemBars()) //Hides system bars (notification bar + navigation bar)

Now lets run our app on both emulators. When we put them side by side, the device with the notch would not completely go fullscreen, while the device without the notch displays in full screen perfectly.

10000201000003B800000095DF87080E6AE72B07.png

Fullscreen with Notch

For systems with a notch, there are two more things that we must do to display our App in full screen.

  1. Set the top-level Window object to display behind the System bars (notification and navigation bars). At the end of the onCreate() callback, add the code below.

     //Enables Edge 2 Edge, allowing the app window to extend behind the system bars
     WindowCompat.setDecorFitsSystemWindows(window, false)
  2. Add an <item> with the name android:windowLayoutInDisplayCutoutMode to the extended App Theme (themes.xml (v27)). This property was introduced on Android 8.1 to support display cutout in XML. Add the <item> below inside <style> in the themes.xml (v27) file to set the mode to shortEdges.

            <item name="android:windowLayoutInDisplayCutoutMode">
     <!--            default-->
                shortEdges
     <!--            never-->
     <!--            always-->
            </item>

Optionally, you can also modify layoutInDisplayCutoutMode programmatically by setting a new value to window.attributes.layoutInDisplayCutoutMode.

If we run the App now, we can see that the text now takes advantage of the entire screen space.

10000201000001BE0000008485C1F6BC981FED36.png

We also know that the text displays behind the notch by looking at the cutoff character h and the resuming character S. The characters behind the notch are IiJjKkLlMmNnOoPpQqRr. This is similar to the Youtube app (need to emulate pinch-to-zoom gesture first).

1000020100000367000001EC65195EBE3B3A5C4D.png

Solution Code

MainActivity.kt

package com.example.daniwebcutoutarea

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       //Hide the system bars
       val windowInsetsController: WindowInsetsControllerCompat? = ViewCompat.getWindowInsetsController(window.decorView) //gets WindowInsetsController
       windowInsetsController?.hide(WindowInsetsCompat.Type.systemBars()) //Hides system bars (notification bar + navigation bar)

       //Enables Edge 2 Edge, allowing the app window to extend behind the system bars
       WindowCompat.setDecorFitsSystemWindows(window, false)
   }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <TextView
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/super_long_text"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Cutout Area</string>
   <string name="super_long_text">AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz</string>
</resources>

themes.xml

<resources xmlns:tools="http://schemas.android.com/tools">
   <!-- Base application theme. -->
   <style name="Theme.DaniwebCutoutArea" parent="Theme.MaterialComponents.DayNight.NoActionBar">
       <!-- Primary brand color. -->
       <item name="colorPrimary">@color/purple_500</item>
       <item name="colorPrimaryVariant">@color/purple_700</item>
       <item name="colorOnPrimary">@color/white</item>
       <!-- Secondary brand color. -->
       <item name="colorSecondary">@color/teal_200</item>
       <item name="colorSecondaryVariant">@color/teal_700</item>
       <item name="colorOnSecondary">@color/black</item>
       <!-- Status bar color. -->
       <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
       <!-- Customize your theme here. -->
   </style>
   <style name="AppTheme" parent="Theme.DaniwebCutoutArea"/>
</resources>

themes.xml (v27)

<resources xmlns:tools="http://schemas.android.com/tools">
   <!-- Base application theme. -->
   <style name="AppTheme" parent="Theme.DaniwebCutoutArea">
<!--         Customize your theme here.-->
       <item name="android:windowLayoutInDisplayCutoutMode">
<!--            default-->
           shortEdges
<!--            never-->
<!--            always-->
       </item>
   </style>
</resources>
Summary

We have learned how to implement support for devices with display cutouts. Even though we only covers the short edge mode in this tutorial, the concept applies similarly to other display modes.

  1. Default: Only renders into the cutout area in landscape mode.
  2. Always: Always extend into the cutout area on both short and long edges. I have not seen any device with a camera cutout on the long edge. The only examples that I can think of are those curved Galaxy Edge devices.
  3. Never: Never extend into the cutout area.
  4. Short edge: Always extend into the cutout area on short edges only.

The full project code can be found here.

Android Native – How to Create Espresso Tests

Introduction

In Android development, Espresso can be combined with Junit to create powerful UI automation tests. In this tutorial, we will learn how to create Espresso tests.

Goals

At the end of the tutorial, you would have learned:

  1. How to create Espresso tests.
Tools Required
  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
  1. Basic Android.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Replace the content of activity_main.xml with the layout XML below.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <EditText
            android:id="@+id/editText_plaintTextInput"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp"
            android:hint="@string/input_text_here"
            android:inputType="text"
            app:layout_constraintBottom_toTopOf="@id/button_submit"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <Button
            android:id="@+id/button_submit"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/button"
            app:layout_constraintBottom_toTopOf="@id/textView_finalText"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/editText_plaintTextInput" />
    
        <TextView
            android:id="@+id/textView_finalText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="@string/no_value"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/button_submit" />
    
     </androidx.constraintlayout.widget.ConstraintLayout>
  3. Replace strings.xml with the xml below.

     <resources>
        <string name="app_name">Daniweb Espresso Test</string>
        <string name="button">SUBMIT</string>
        <string name="no_value">No Value</string>
        <string name="input_text_here">Input text here</string>
     </resources>
Getting familiar with the demo app

Even though all of the boilerplate code has been provided for this tutorial, let us quickly go over the state of the demo app right now.

First, launch the app, and we can see that there are three elements in our application:

  1. An EditText element for users to input plain text. It provides a hint of Input text here to let the user know that they should provide some text value here.
  2. A Button element for submitting the text. The actual button action is not yet implemented. Once implemented, we would want the text value(android:text) of EditText to be set as the TextView text value as well.
  3. A TextView element for displaying the text value submitted from the EditText element. If the TextView android:text value is empty, it will display No Value.

demo_app.png

The Espresso library

When Espresso is mentioned together with modern Android development, it can mean two things:

  1. The androidx.test.espresso package. This package includes the core classes of the library. Other subpackages of Espresso can be used if you need support for more complicated workflows. For example, if you need to test WebView, you would need the androidx.test.espresso.web.* packages.
  2. The androidx.test.espresso.Espresso class. This class is the main entry point for Espresso tests.
The Espresso Test

The flow of an Espresso test is quite simple. They are:

  1. Start the test by using one of the static methods from the Espresso class. For this tutorial, we will use the Espresso.onView() method.
  2. Providing a Hamcrest matcher object to onView() (or other entry points such as onData()). You can obtain pre-made Matcher objects from the ViewMatchers class( androidx.test.espresso.matcher.ViewMatchers).
  3. onView() will return a ViewInteraction object. Via this ViewInteraction object, we can call the method perform() to perform actions on the underlying View. The method perform() takes a varargs of ViewAction objects. Each ViewAction object represents an action to perform. You can obtain pre-implemented ViewAction objects from the ViewActions class(androidx.test.espresso.action.ViewActions).
  4. The call to perform() will return the same ViewInteraction object, which we can then call its instance method check() to provide assertions to verify whether the test has passed. Each assertion is encapsulated in a ViewAssertion object. One convenient way to obtain ViewAssertion objects is to use static methods of the ViewAssertions class(androidx.test.espresso.assertion.ViewAssertions).
Creating the Test

For this tutorial, we will attempt to use Espresso in an instrumentation test. There should already be a test class under androidTest, so we can just re-use that file for simplicity. The class name in my project is ExampleInstrumentedTest. To create our own Espresso test, perform the steps below:

  1. Copy and paste this JUnit rule inside the ExampleInstrumentedTest class. This is a special rule used with AndroidJunitRunner that starts the Activity (MainActivity in our case) before each test and stops the Activity at the end of each test. Check the code comment if you want to read more about this Rule, but discussing this Rule is out of scope for this tutorial.

     /**
     * Check https://developer.android.com/training/testing/junit-rules#activity-test-rule
     */
     @get:Rule
     val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)
  2. Remove the default useAppContext() test from the class because we do not need it.

  3. Paste the code below into the test class.

     @Test
     fun testFinalTextView() {
        val testString = "Daniweb Espresso Test"
    
        onView(withId(R.id.editText_plaintTextInput)) //Obtains the TextEdit element
            .perform(typeText(testString)) //Add text to the TextEdit field
    
        onView(withId(R.id.button_submit)) //Obtains the Button element
            .perform(click()) //Click on the Submit button
    
        onView(withId(R.id.textView_finalText)) //Obtains the TextView element
            .check(matches(withText(testString))) //Checks if its value matches the submitted value
     }

The method testFinalTextView() attempts to replicate the expected flow. The code comments above explains what the code does on each line.

Run the Test

We are ready to run the test.

  1. Right-click on the Test class, in the Project view.
  2. RunExampleInstrumentedTest.

Espresso_Test.gif

Then you can see that the test will fail with the error:

Expected: an instance of android.widget.TextView and view.getText() with or without transformation to match: is "Daniweb Espresso Test"
     Got: view.getText() was "" transformed text was ""

This behavior is expected because we have not implemented any logic for the Submit button yet. To quickly implement the logic, replace the current onCreate() function in MainActivity.kt with the new version below.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)

   val editText = findViewById<EditText>(R.id.editText_plaintTextInput)
   val button = findViewById<Button>(R.id.button_submit)
   val finalText = findViewById<TextView>(R.id.textView_finalText)

   button.setOnClickListener {
       finalText.text = editText.text
   }
}
Run the Test again

With the logic implemented, we can run the test again and see it passing.

Espresso_Test_Passed.gif

Solution Code

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <EditText
       android:id="@+id/editText_plaintTextInput"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_marginStart="16dp"
       android:layout_marginEnd="16dp"
       android:hint="@string/input_text_here"
       android:inputType="text"
       app:layout_constraintBottom_toTopOf="@id/button_submit"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

   <Button
       android:id="@+id/button_submit"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/button"
       app:layout_constraintBottom_toTopOf="@id/textView_finalText"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintHorizontal_bias="0.5"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@id/editText_plaintTextInput" />

   <TextView
       android:id="@+id/textView_finalText"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:hint="@string/no_value"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintHorizontal_bias="0.5"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@id/button_submit" />

</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml

<resources>
   <string name="app_name">Daniweb Espresso Test</string>
   <string name="button">SUBMIT</string>
   <string name="no_value">No Value</string>
   <string name="input_text_here">Input text here</string>
</resources>

MainActivity.kt

package com.example.daniwebespressotest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val editText = findViewById<EditText>(R.id.editText_plaintTextInput)
       val button = findViewById<Button>(R.id.button_submit)
       val finalText = findViewById<TextView>(R.id.textView_finalText)

       button.setOnClickListener {
           finalText.text = editText.text
       }
   }
}

ExampleInstrumentedTest.kt

package com.example.daniwebespressotest

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*
import org.junit.Rule

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {

   /**
    * Check https://developer.android.com/training/testing/junit-rules#activity-test-rule
    */
   @get:Rule
   val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)

   @Test
   fun testFinalTextView() {
       val testString = "Daniweb Espresso Test"

       onView(withId(R.id.editText_plaintTextInput)) //Obtains the TextEdit element
           .perform(typeText(testString)) //Add text to the TextEdit field

       onView(withId(R.id.button_submit)) //Obtains the Button element
           .perform(click()) //Click on the Submit button

       onView(withId(R.id.textView_finalText)) //Obtains the TextView element
           .check(matches(withText(testString))) //Checks if its value matches the submitted value
   }
}
Summary

Congratulations, we have learned how to create UI Automation tests with Espresso. The full project code can be found here https://github.com/dmitrilc/DaniwebEspressoTest.

Android Native – How to create UI Automator tests

Introduction

UI Automator is a library that allows you to create tests that can interact with other components besides your App, such as Settings or other Android components. In this tutorial, we will learn how to incorporate UI Automator into our tests.

Goals

At the end of the tutorial, you would have learned:

  1. How to use UI Automator to create tests.
Tools Required
  1. Android Studio. The version used in this tutorial is Arctic Fox 2020.3.1 Patch 4.
Prerequisite Knowledge
  1. Basic Android.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Give the default TextView the android:id of textView_helloWorld

  3. Add the dependency for Ui Automator to your module gradle file.

     androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
UI Automator Overview

UI Automator simply refers to a group of classes belonging to the androidx.test.uiautomator package. The primary classes that you need to know in this package are:

  1. UiDevice: allows you to access device state and simulate user actions. to simulate user actions, you can use methods such as click(), drag(), pressX(), etc.
  2. UiObject: represents a view. You can obtain UiObject instances via findObject() methods from the UiDevice class. UiObjects public constructors are deprecated. You can also reuse this object for different views matching the same UiSelector.
  3. UiSelector: a filter object used to create a UiObject. If you are familiar with Hamcrest Matchers, then this is somewhat similar to the ViewMatchers class (androidx.test.espresso.matcher.ViewMatchers).
Demo App Test Flow

We are only using the default Empty Activity project to simplify the tutorial. For this UI Automator test, we will attempt to switch the App to the Dark theme using the system-wide Display setting on Android. The launcher used in the test is the default Android 12 launcher on a Pixel emulator (Pixel 4 XL API 31).

Our test includes 10 steps, when not including the Activity launcher step.

  1. When the demo App is the foreground App, press the Home button.
  2. Swipe up from anywhere on the home screen to open the launcher.
  3. Open the Settings app.
  4. After the Settings app is opened, swipe up a little to scroll down on the list.
  5. Open the Display setting.
  6. Switch on Dark theme.
  7. Open the Recents button.
  8. Swipe right to put our app into focus.
  9. Resume our App.
  10. Verify that our App is in Dark mode.

Refer to the picture below for a preview of the desired flow of the test.

1.jpg

UI Automator Viewer

Before we are able to write any code, we must use the UI Automator Viewer (uiautomatorviewer) tool to inspect the UI elements that we want to test. As of this writing, this tool can only be launched from the CLI.

uiautomatorviewer is part of the Android SDK and not part of IntelliJ, so you can find it where the SDK is installed. On a Windows machine, for my Android Studio build, the default location is at:

C:\Users\%USERNAME%\AppData\Local\Android\Sdk\tools\bin\uiautomatorviewer.bat

If you are not using Windows, you can find the location of the Android SDK in IntelliJ by doing:

  1. File > Settings > Appearance & Behavior > System Settings > Android SDK
  2. Find the Android SDK Location property.

You can also just search for Android SDK Location using IntelliJs Search Everywhere functionality (press double Shift).

Launching UI Automator Viewer

From the builtin IntelliJ terminal, navigate to the UI Automator Viewer directory with the command below.

cd C:\Users\%USERNAME%\AppData\Local\Android\Sdk\tools\bin\

Launch the uiautomatorviewer by calling uiautomatorviewer.bat.

tools\bin\uiautomatorviewer.bat

As of this writing, uiautomatorviewer only works if your JAVA_HOME is set to Java 8. You can also just create a wrapper around the uiautomator.bat file to modify JAVA_HOME just for the terminal session if you do not want to change your system-wide JAVA_HOME. The same workaround also applies to Linux/MacOS when JAVA_HOME is not set to Java 8.

After launching the uiautomatorviewer, make sure that your Emulator is running. Press the Device Screenshot button at the left corner of the uiautomatorviewer tool.

2.png

It will generate a static XML tree of UI elements that is currently on the screen.

3.png

Using this tool, you can extract details about an element and use them to write your test code. Some useful node attributes are text, resource-id, class, package, content-desc. In the next section, we will see what a complete UI Automator test looks like.

Write Test Code

We are now ready to write our test.

  1. In the ExampleInstrumentedTest file, remove the useAppContext() test method.

  2. Set the ActivityScenarioRule in the class to launch our MainActivity when the test starts.

     /**
     * Check https://developer.android.com/training/testing/junit-rules#activity-test-rule
     */
     @get:Rule
     val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)
  3. Copy and paste the test function below into your test class.

     @Test
     fun testDarkModeSwitch(){
        //Obtains the instrumented device
        val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    
        device.pressHome() //Press the Home button
    
        val homeScreen = device.findObject(UiSelector() //Starts the findObject query
            .resourceId("android:id/content") //Tries to match the element resource id
            .className(FrameLayout::class.java) //Tries to match the element class name
            .packageName("com.google.android.apps.nexuslauncher")) //Tries to match the package name. UiDevice.getPackageName might be cleaner
    
        homeScreen.waitForExists(1_000) //this is one option to wait for the View to load
    
        homeScreen.swipeUp(10) //Swipes up to open the Launcher
    
        val settingsIcon = device.findObject(UiSelector()
            .resourceId("com.google.android.apps.nexuslauncher:id/icon")
            .className(TextView::class.java)
            .packageName("com.google.android.apps.nexuslauncher")
            .descriptionContains("Settings")
        )
    
        settingsIcon.waitForExists(1_000)
    
        settingsIcon.click()
    
        //UiScrollable provides better API to interact with the Settings RecyclerView
        val settingsView = UiScrollable(UiSelector()
            .resourceId("com.android.settings:id/main_content_scrollable_container")
            .className(ScrollView::class.java)
            .packageName("com.android.settings")
        )
    
        settingsView.waitForExists(1_000)
    
        val displayOption = device.findObject(UiSelector()
            .text("Display")
            .resourceId("android:id/title")
            .className(TextView::class.java)
            .packageName("com.android.settings")
        )
    
        settingsView.scrollIntoView(displayOption)
    
        displayOption.waitForExists(1_000)
    
        displayOption.click()
    
        val darkThemeSwitch = device.findObject(UiSelector()
            .resourceId("com.android.settings:id/switchWidget")
            .className(Switch::class.java)
            .packageName("com.android.settings")
            .descriptionContains("Dark theme")
        )
    
        darkThemeSwitch.waitForExists(1_000)
    
        darkThemeSwitch.click()
    
        device.pressRecentApps() //Recents is black box because uiautomatorviewer is unable to spy the Recents View
    
        device.pressKeyCode(KeyEvent.KEYCODE_APP_SWITCH)
    
        val app = device.findObject(UiSelector()
            .packageName("com.example.daniwebuiautomatortest")
        )
    
        app.waitForExists(1_000)
    
        val context = InstrumentationRegistry.getInstrumentation().targetContext
    
        val isDark = context
            .resources
            .configuration.
            isNightModeActive
    
        assert(isDark)
     }

A few notes about the code snippet above.

  1. It is quite long and the comments are too verbose, so it can be hard to read in real life, but I find it easier to read in a blog post because the method names are mostly self-explanatory. In real code, you should perform some refactoring to improve readability.
  2. The UI Automator Viewer is unable to spy the Navigation Buttons and the Recents screen, so pressKeyCode() was used as a workaround.
  3. My UiSelector queries were way overkill, but I mainly wanted to introduce the API.
Run the Test

We are now ready to run the test. Right-click on ExampleInstrumentedTest in the Project view and select Run ExampleInstrumentedTest.

UI_Test_Passed_6.gif

And we can see that the test has passed. The animation above was compressed to save bandwidth; the actual test takes about 7-12 seconds on my computer.

Solution Code

ExampleInstrumentedTest.kt

package com.example.daniwebuiautomatortest

import android.view.KeyEvent
import android.widget.FrameLayout
import android.widget.ScrollView
import android.widget.Switch
import android.widget.TextView
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.*

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Rule

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {

   /**
    * Check https://developer.android.com/training/testing/junit-rules#activity-test-rule
    */
   @get:Rule
   val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)

   @Test
   fun testDarkModeSwitch(){
       //Obtains the instrumented device
       val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

       device.pressHome() //Press the Home button

       val homeScreen = device.findObject(UiSelector() //Starts the findObject query
           .resourceId("android:id/content") //Tries to match the element resource id
           .className(FrameLayout::class.java) //Tries to match the element class name
           .packageName("com.google.android.apps.nexuslauncher")) //Tries to match the package name. UiDevice.getPackageName might be cleaner

       homeScreen.waitForExists(1_000) //this is one option to wait for the View to load

       homeScreen.swipeUp(10) //Swipes up to open the Launcher

       val settingsIcon = device.findObject(UiSelector()
           .resourceId("com.google.android.apps.nexuslauncher:id/icon")
           .className(TextView::class.java)
           .packageName("com.google.android.apps.nexuslauncher")
           .descriptionContains("Settings")
       )

       settingsIcon.waitForExists(1_000)

       settingsIcon.click()

       //UiScrollable provides better API to interact with the Settings RecyclerView
       val settingsView = UiScrollable(UiSelector()
           .resourceId("com.android.settings:id/main_content_scrollable_container")
           .className(ScrollView::class.java)
           .packageName("com.android.settings")
       )

       settingsView.waitForExists(1_000)

       val displayOption = device.findObject(UiSelector()
           .text("Display")
           .resourceId("android:id/title")
           .className(TextView::class.java)
           .packageName("com.android.settings")
       )

       settingsView.scrollIntoView(displayOption)

       displayOption.waitForExists(1_000)

       displayOption.click()

       val darkThemeSwitch = device.findObject(UiSelector()
           .resourceId("com.android.settings:id/switchWidget")
           .className(Switch::class.java)
           .packageName("com.android.settings")
           .descriptionContains("Dark theme")
       )

       darkThemeSwitch.waitForExists(1_000)

       darkThemeSwitch.click()

       device.pressRecentApps() //Recents is black box because uiautomatorviewer is unable to spy the Recents View

       device.pressKeyCode(KeyEvent.KEYCODE_APP_SWITCH)

       val app = device.findObject(UiSelector()
           .packageName("com.example.daniwebuiautomatortest")
       )

       app.waitForExists(1_000)

       val context = InstrumentationRegistry.getInstrumentation().targetContext

       val isDark = context
           .resources
           .configuration.
           isNightModeActive

       assert(isDark)
   }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <TextView
       android:id="@+id/textView_helloWorld"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="Hello World!"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

build.gradle

plugins {
   id 'com.android.application'
   id 'kotlin-android'
}

android {
   compileSdk 31

   defaultConfig {
       applicationId "com.example.daniwebuiautomatortest"
       minSdk 21
       targetSdk 31
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       }
   }
   compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
   }
   kotlinOptions {
       jvmTarget = '1.8'
   }
}

dependencies {

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.0'
   implementation 'com.google.android.material:material:1.4.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
   testImplementation 'junit:junit:4.+'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
   androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}
Summary

We have learned how to use UI Automator library to create tests as well as how to use the uiautomatorviewer tool to design tests. The full project code can be found here https://github.com/dmitrilc/DaniwebUiAutomatorTest