{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Introduction
Hi everyone, In this article, we’re going to explore how to paginate with the Paging 3 library for Huawei Site Kit and, we’ll use Kotlin in Android Studio. Paging library is a generic solution for pagination. It helps us load and display data from any local or network sources. We’ve used Site Kit as a sample.
Why we need Pagination?
While we get data from any source, pagination provides us getting small chunks of data at a time. That way, we don’t load and display to the user extra data. As a result, we reduce unnecessary network requests and system resource usage.
What is Paging 3 Library?
Paging 3 library is a part of Android Jetpack and enables to load large sets of data gradually. It suits with Android App Architecture and integrates easily with other Jetpack components.
We’ve listed some of the features of the library below.
It supports Kotlin coroutines, Flow, RxJava, and LiveData.
It works with RecyclerView in an integrated manner.
It caches the paged data in-memory to use system resources efficiently.
It makes simple error handling, page refreshing, and retry.
Huawei Site Kit
Site Kit provides place search services including keyword search, nearby place search, place detail search, and place search suggestion, helping your app provide convenient place-related services to attract more users and improve user loyalty.
We’re not going to go into the details of adding Sit Kit to a project. You can follow the instructions to add Site Kit to your project via official docs or codelab.
Our Sample Pagination Project
In this project, we’ll develop a sample app showing nearby places of the user.
You can see the package structure of our application in the image below.
We used MVVM architecture and recent libraries.
Setup the Project
Add the necessary dependencies to build.gradle (app level)
Code:
// HMS Core
implementation 'com.huawei.agconnect:agconnect-core:1.4.2.300'
// Huawei Site Kit
implementation 'com.huawei.hms:site:5.1.0.300'
// Hilt for Dependency Injection
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
// Coroutines for Asynchronous Programming
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
def lifecycle_version = "2.2.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Annotation processor
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
// to use viewModels() property delegate
implementation "androidx.activity:activity-ktx:1.1.0"
// Paging 3 for pagination
implementation "androidx.paging:paging-runtime:3.0.0-alpha13"
Layout Files
activity_main.xml includes RecyclerView to display nearby places, a progress bar for the loading process, and a button for retry the data.
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"
android:orientation="vertical"
tools:context=".ui.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerSite"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/retryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Retry"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
item_site.xml -> We need a item for RecyclerView.
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="64dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:background="#f9f9f9"
android:orientation="vertical">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_place"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/siteName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="Eiffel Tower" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.2"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/siteDistance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/siteName"
tools:text="800m" />
</androidx.constraintlayout.widget.ConstraintLayout>
Model Classes
We have 2 model classes in our app named Site and Result. Site class is a part of the Site Kit so, we don’t create it.
Result.kt -> Result is a wrapper class which contains Success and Error.
Code:
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
Source Of Data
Firstly we need a source to get data so, use the Site Kit as a source. We need a location to use the NearbyLocation feature of the Site Kit and, we selected a mock location near the Eiffel Tower.
HuaweiSiteSource.kt -> With the help of the getNearbyPlaces() function, we get the Site List. This suspend function takes the page number as a parameter. If an error occurs in this process, it returns the Error as a result.
Code:
@Singleton
class HuaweiSiteSource @Inject constructor(@ApplicationContext context: Context) {
private val PAGE_SIZE = 20
private val searchService by lazy {
SearchServiceFactory.create(
context,
URLEncoder.encode(Constant.API_KEY, "UTF-8")
)
}
suspend fun getNearbyPlaces(
page: Int,
): Result<List<Site>> {
val eiffelTowerCoordinate = Coordinate(48.858093, 2.294694)
return suspendCoroutine { cont ->
val radius = 10000
val request = NearbySearchRequest()
request.location = eiffelTowerCoordinate
request.radius = radius
request.pageSize = PAGE_SIZE
request.pageIndex = page
searchService?.nearbySearch(
request,
object : SearchResultListener<NearbySearchResponse> {
override fun onSearchResult(nearbySearchResponse: NearbySearchResponse?) {
val siteList = nearbySearchResponse?.sites
siteList?.let {
cont.resume(Result.Success(it))
}
}
override fun onSearchError(searchStatus: SearchStatus?) {
cont.resume(Result.Error(Exception(searchStatus?.errorMessage)))
}
})
}
}
}
Then, we create the SitePagingSource.kt and implement a PagingSource<Key, Value> to define a data source. It takes a Key and Value as parameters. The Key is the index numbers for pages and, Value is the type of data loaded. We specified Int as the page number, Site as the data type.
PagingSource requires us to implement load() and getRefreshKey() functions.
load() is a suspend function. So, we can make our network or local database requests without blocking the main thread.
getRefreshKey() provides a Key for the initial load for the next Paging Source due to invalidation of this PagingSource. The last accessed position can be retrieved via "state.anchorPosition" so, we used the "state.anchorPosition".
Code:
@Singleton
class SitePagingSource(
private val huaweiSiteSource: HuaweiSiteSource
) : PagingSource<Int, Site>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Site> {
return try {
val page = params.key ?: FIRST_PAGE_INDEX
val result = huaweiSiteSource.getNearbyPlaces(page)
return when (result) {
is Result.Success -> LoadResult.Page(
data = result.data,
prevKey = if (page == FIRST_PAGE_INDEX) null else page - 1,
nextKey = if (result.data.isEmpty() || page >= LAST_PAGE_INDEX) null else page + 1
)
else -> LoadResult.Error(Throwable("Error occurred"))
}
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Site>): Int? {
return state.anchorPosition
}
companion object {
const val FIRST_PAGE_INDEX = 1
const val LAST_PAGE_INDEX = 3
}
}
HuaweiRepository.kt -> getDefaultPageConfig() provides us to configure our settings such as page size, placeholders, initialloadsize etc.
getSiteListAsFlow() calls the load() method from the SitePagingSource to get data and, we’re transferring the data through the flow.
Code:
@Singleton
class HuaweiRepository @Inject constructor(
private val huaweiSiteSource: HuaweiSiteSource
) {
fun getSiteListAsFlow(
pagingConfig: PagingConfig = getDefaultPageConfig()
): Flow<PagingData<Site>> {
return Pager(
config = pagingConfig,
pagingSourceFactory = { SitePagingSource(huaweiSiteSource) }
).flow
}
private fun getDefaultPageConfig(): PagingConfig {
return PagingConfig(
pageSize = 20,
enablePlaceholders = false
)
}
}
ViewModel
MainViewModel.kt -> fetchSiteList() helps us getting the data and cache it to survive configuration changes like screen rotation.
Code:
@HiltViewModel
class MainViewModel @Inject constructor(
private val huaweiRepository: HuaweiRepository
) : ViewModel() {
fun fetchSiteList(): Flow<PagingData<Site>> {
return huaweiRepository.getSiteListAsFlow()
.cachedIn(viewModelScope)
}
}
Before get into the showing places on the view, I want to mention about RecyclerView Adapter. Paging3 has a special Adapter to list items in the recyclerview.
SiteAdapter.kt -> In this class, we extend our class from PagingDataAdapter. It takes two parameters; a Model class (Site) and a ViewHolder (SiteViewHolder). Also, we used ViewBinding to interact with the views that have an assigned id value in a null-safe and type-safe way. And, we applied the Higher-order function for item clicks.
Code:
class SiteAdapter : PagingDataAdapter<Site, SiteAdapter.SiteViewHolder>(REPO_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SiteViewHolder {
val binding = ItemSiteBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SiteViewHolder(binding)
}
companion object {
private val REPO_COMPARATOR = object : DiffUtil.ItemCallback<Site>() {
override fun areItemsTheSame(oldItem: Site, newItem: Site): Boolean {
return oldItem.siteId == newItem.siteId
}
override fun areContentsTheSame(oldItem: Site, newItem: Site): Boolean {
return oldItem.equals(newItem)
}
}
}
private var onItemClickListener: ((Site) -> Unit)? = null
override fun onBindViewHolder(holder: SiteViewHolder, position: Int) {
val site = getItem(position) ?: return
holder.binding.apply {
this.siteName.text = site.name
this.siteDistance.text = "${site.distance}m"
}
holder.itemView.apply {
setOnClickListener {
onItemClickListener?.let { it(site) }
}
}
}
inner class SiteViewHolder(val binding: ItemSiteBinding) : RecyclerView.ViewHolder(binding.root)
fun setOnItemClickListener(listener: (Site) -> Unit) {
onItemClickListener = listener
}
}
View
MainActivity.kt -> Firstly, we set up our RecyclerView and SiteAdapter. After that, we collected the data from ViewModel and passed it to the adapter. Finally, we observed the load state with the help of the addLoadStateListener() method. When there is a change in the load state of the adapter, it notifies us. According to these states, we can change our UI status such as loading, displaying, or retrying.
Code:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
private lateinit var binding: ActivityMainBinding
private val siteAdapter by lazy {
SiteAdapter().apply {
setOnItemClickListener {
Toast.makeText(
[email protected],
"Clicked Site Name : ${it.name}",
Toast.LENGTH_SHORT
).show()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.recyclerSite.apply {
layoutManager = LinearLayoutManager(context)
adapter = siteAdapter
layoutManager = layoutManager
}
binding.retryButton.setOnClickListener { siteAdapter.retry() }
siteAdapter.addLoadStateListener { loadState ->
when (loadState.source.refresh) {
is LoadState.NotLoading -> {
binding.recyclerSite.isVisible = true
binding.progressBar.isVisible = false
binding.retryButton.isVisible = false
}
is LoadState.Loading -> {
binding.progressBar.isVisible = true
binding.retryButton.isVisible = false
}
is LoadState.Error -> {
binding.progressBar.isVisible = false
binding.retryButton.isVisible = true
}
}
val errorState = loadState.source.append as? LoadState.Error
?: loadState.append as? LoadState.Error
errorState?.let {
Toast.makeText(this, errorState.error.localizedMessage, Toast.LENGTH_SHORT).show()
}
}
lifecycleScope.launch {
viewModel.fetchSiteList().collectLatest { pagingData ->
viewModel.fetchSiteList().distinctUntilChanged().collectLatest {
siteAdapter.submitData(it)
}
}
}
}
Tips & Tricks
⚠While using the Flow, make sure you import correctly. Adding unambiguous imports on the fly in Android Studio can cause the adding of wrong imports.
⚠ Paging3 supports LiveData and RxJava beside Flow.
⚠Configuring your page size relies upon how your data is being loaded and used. Smaller page sizes improve memory usage, latency. Larger pages generally improve loading throughput.
Conclusion
In summary, we have developed a simple app that shows the nearby places of the user. We’ve used the Paging 3 library to make it easier to work with large sets of data and Huawei Site kit to get nearby places information of the users. Please do not hesitate to ask your questions as a comment.
Thank you for your time and dedication. I hope it was helpful. See you in other articles!
References
Paging 3 Official Documentation
Huawei Site Kit Official Documentation
Related
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Introduction
Hi everyone, In this article, we’ll explore how to develop a download manager app using the Huawei Network Kit. And, we’ll use Kotlin as a programming language in Android Studio.
Huawei Network Kit
Network Kit provides us to upload or download files with additional features such as multithreaded, concurrent, resumable uploads and downloads. Also, it allows us to perform our network operations quickly and safely. It provides a powerful interacting with Rest APIs and sending synchronous and asynchronous network requests with annotated parameters. Finally, we can use it with other Huawei kits such as hQUIC Kit and Wireless Kit to get faster network traffic.
If you want to learn how to use Network Kit with Rest APIs, you can check my article about it.
Download Manager — Sample App
In this project, we’re going to develop a download manager app that helps users download files quickly and reliably to their devices.
Key features:
Start, Pause, Resume or Cancel downloads.
Enable or Disable Sliced Download.
Set’s the speed limit for downloading a file.
Calculate downloaded size/total file size.
Calculate and display download speed.
Check the progress in the download bar.
Support HTTP and HTTPS protocols.
Copy URL from clipboard easily
We started a download task. Then, we paused and resumed it. When the download is finished, it showed a snackbar to notify us.
Setup the Project
We’re not going to go into the details of integrating Huawei HMS Core into a project. You can follow the instructions to integrate HMS Core into your project via official docs or codelab. After integrating HMS Core, let’s add the necessary dependencies.
Add the necessary dependencies to build.gradle (app level).
Java:
dependencies {
...
// HMS Network Kit
implementation 'com.huawei.hms:filemanager:5.0.3.300'
// For runtime permission
implementation 'androidx.activity:activity-ktx:1.2.3'
implementation 'androidx.fragment:fragment-ktx:1.3.4'
...
}
Let’s add the necessary permissions to our manifest.
XML:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.huawei.networkkitsample">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
We added the Internet Permission to access the Internet and the storage permissions to read and write data to the device memory. Also, we will dynamically request the permissions at runtime for storage permissions on devices that runs Android 6.0 (API Level 23) or higher.
Configure the AndroidManifest file to use clear text traffic
If you try to download a file from an HTTP URL on Android 9.0 (API level 28) or higher, you’ll get an error like this:
Code:
ErrorCodeFromException errorcode from resclient: 10000802,message:CLEARTEXT communication to ipv4.download.thinkbroadband.com(your url) not permitted by network security policy
Because cleartext support is disabled by default on Android 9.0 or higher. You should add the android:usesClearTextTraffic="true" flag in the AndroidManifest.xml file. If you don’t want to enable it for all URLs, you can create a network security config file. If you are only working with HTTPS files, you don’t need to add this flag.
XML:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.huawei.networkkitsample">
...
<application
...
android:usesCleartextTraffic="true"
...
</application>
</manifest>
Layout File
activity_main.xml is the only layout file in our project. There are:
A TextInputEditText to enter URL,
Four buttons to control the download process,
A button to paste URL to the TextInputEditText,
A progress bar to show download status,
A seekbar to adjust download speed limit,
A checkbox to enable or disable the “Slide Download” feature,
TextViews to show various information.
activity_main.xml
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/main_constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<Button
android:id="@+id/startDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Start"
app:layout_constraintEnd_toStartOf="@+id/pauseDownload_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/enableSliced_checkBox" />
<ProgressBar
android:id="@+id/downloadProgress_progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:progressBackgroundTint="@color/design_default_color_primary_variant"
android:progressTint="@color/design_default_color_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/percentProgress_textView" />
<TextView
android:id="@+id/percentProgress_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="0%"
app:layout_constraintStart_toStartOf="@+id/downloadProgress_progressBar"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />
<TextView
android:id="@+id/finishedSize_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="0"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintStart_toEndOf="@+id/percentProgress_textView"
tools:text="2.5" />
<TextView
android:id="@+id/sizeSeparator_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="/"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintStart_toEndOf="@+id/finishedSize_textView" />
<TextView
android:id="@+id/totalSize_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="0"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintStart_toEndOf="@+id/sizeSeparator_textView"
tools:text="29.6 MB" />
<SeekBar
android:id="@+id/speedLimit_seekBar"
style="@style/Widget.AppCompat.SeekBar.Discrete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:max="7"
android:progress="7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fixSpeedLimit_textView" />
<TextView
android:id="@+id/fixSpeedLimit_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:text="Download Speed Limit:"
app:layout_constraintStart_toStartOf="@+id/speedLimit_seekBar"
app:layout_constraintTop_toBottomOf="@+id/remainingTime_textView" />
<TextView
android:id="@+id/speedLimit_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Limitless"
app:layout_constraintBottom_toBottomOf="@+id/fixSpeedLimit_textView"
app:layout_constraintStart_toEndOf="@+id/fixSpeedLimit_textView" />
<TextView
android:id="@+id/currentSpeed_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0 kB/s"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintEnd_toEndOf="@+id/downloadProgress_progressBar"
tools:text="912 kB/s" />
<Button
android:id="@+id/pauseDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pause"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/startDownload_button"
app:layout_constraintTop_toTopOf="@+id/startDownload_button" />
<Button
android:id="@+id/resumeDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Resume"
app:layout_constraintEnd_toEndOf="@+id/startDownload_button"
app:layout_constraintStart_toStartOf="@+id/startDownload_button"
app:layout_constraintTop_toBottomOf="@+id/startDownload_button" />
<Button
android:id="@+id/cancelDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Cancel"
app:layout_constraintEnd_toEndOf="@+id/pauseDownload_button"
app:layout_constraintStart_toStartOf="@+id/pauseDownload_button"
app:layout_constraintTop_toBottomOf="@+id/pauseDownload_button" />
<TextView
android:id="@+id/remainingTime_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0s left"
app:layout_constraintStart_toStartOf="@+id/downloadProgress_progressBar"
app:layout_constraintTop_toBottomOf="@+id/downloadProgress_progressBar" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/pasteClipboard_imageButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/url_textInputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="URL"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/pasteClipboard_imageButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
android:background="@android:color/transparent"
android:scaleType="fitXY"
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textInputLayout"
app:srcCompat="@drawable/ic_paste_content" />
<CheckBox
android:id="@+id/enableSliced_checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:checked="true"
android:text="Enable Slice Download"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/speedLimit_seekBar" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
Let’s interpret some of the functions on this page.
onCreate() - Firstly we used viewBinding instead of findViewById. It generates a binding class for each XML layout file present in that module. With the instance of a binding class, we can access the view hierarchy with type and null safety.
Then, we initialized the ButtonClickListeners and the ViewChangeListeners. And we create a FileRequestCallback object. We’ll go into the details of this object later.
startDownloadButton() - When the user presses the start download button, it requests permissions at runtime. If the user allows accessing device memory, it will start the download process.
startDownload() - First, we check the downloadManager is initialized or not. Then, we check if there is a download task or not. getRequestStatus function provides us the result status as INIT, PROCESS, PAUSE and, INVALID.
Code:
If auto-import is active in your Android Studio, It can import the wrong package for the Result Status. Please make sure to import the "com.huawei.hms.network.file.api.Result" package.
The Builder helps us to create a DownloadManager object. We give a name to our task. If you plan to use the multiple download feature, please be careful to give different names to your download managers.
The DownloadManagerBuilder helps us to create a DownloadManager object. We give a tag to our task. In our app, we only allow single downloading to make it simple. If you plan to use the multiple download feature, please be careful to give different tags to your download managers.
When creating a download request, we need a file path to save our file and a URL to download. Also, we can set a speed limit or enable the slice download.
Code:
Currently, you can only set the speed limit for downloading a file. The speed limit value ranges from 1 B/s to 1 GB/s. speedLimit() takes a variable of the type INT as a byte value.
You can enable or disable the sliced download.
Code:
Sliced Download: It slices the file into multiple small chunks and downloads them in parallel.
Finally, we start an asynchronous request with downloadManager.start() command. It takes the getRequest and the fileRequestCallback.
FileRequestCallback object contains four callback methods: onStart, onProgress, onSuccess and onException.
onStart -> It will be called when the file download starts. We take the startTime to calculate the remaining download time here.
onProgress -> It will be called when the file download progress changes. We can change the progress status here.
Code:
These methods run asynchronously. If we want to update the UI, we should change our thread to the UI thread using the runOnUiThread methods.
onSuccess -> It will be called when file download is completed. We show a snackbar to the user after the file download completes here.
onException -> It will be called when an exception occurs.
Code:
onException also is triggered when the download is paused or resumed. If the exception message contains the "10042002" number, it is paused, if it contains the "10042003", it is canceled.
MainActivity.kt
Java:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var downloadManager: DownloadManager
private lateinit var getRequest: GetRequest
private lateinit var fileRequestCallback: FileRequestCallback
private val TAG = "MainActivity"
private var downloadURL = "http://ipv4.download.thinkbroadband.com/20MB.zip"
private var downloadSpeedLimit: Int = 0
private var startTime: Long = 0L
private var isEnableSlicedDownload = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.urlTextInputEditText.setText(downloadURL)
initButtonClickListeners()
initViewChangeListeners()
fileRequestCallback = object : FileRequestCallback() {
override fun onStart(getRequest: GetRequest): GetRequest {
startTime = System.nanoTime()
return getRequest
}
override fun onProgress(getRequest: GetRequest, progress: Progress) {
runOnUiThread {
binding.downloadProgressProgressBar.progress = progress.progress
binding.percentProgressTextView.text = "${progress.progress}%"
convertByteToMb(progress.totalSize)?.let {
binding.totalSizeTextView.text = "$it MB"
}
convertByteToMb(progress.finishedSize)?.let {
binding.finishedSizeTextView.text = it
}
showCurrentDownloadSpeed(progress.speed)
showRemainingTime(progress)
}
}
override fun onSuccess(response: Response<GetRequest, File, Closeable>?) {
if (response?.content != null) {
runOnUiThread {
binding.downloadProgressProgressBar.progress = 100
binding.percentProgressTextView.text = "100%"
binding.remainingTimeTextView.text = "0s left"
convertByteToMb(response.content.length())?.let {
binding.finishedSizeTextView.text = it
binding.totalSizeTextView.text = "$it MB"
}
showSnackBar(binding.mainConstraintLayout, "Download Completed")
}
}
}
override fun onException(
getRequest: GetRequest?,
exception: NetworkException?,
response: Response<GetRequest, File, Closeable>?
) {
if (exception != null) {
val pauseTaskValue = "10042002"
val cancelTaskValue = "10042003"
val errorMessage = exception.message
errorMessage?.let {
if (!it.contains(pauseTaskValue) && !it.contains(cancelTaskValue)) {
Log.e(TAG, "Error Message:$it")
exception.cause?.let { throwable ->
runOnUiThread {
Toast.makeText(
[email protected],
throwable.message,
Toast.LENGTH_SHORT
)
.show()
}
}
}
}
}
}
}
}
private fun initViewChangeListeners() {
binding.speedLimitSeekBar.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
downloadSpeedLimit = calculateSpeedLimitAsByte(progress)
showDownloadSpeedLimit(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
}
})
binding.enableSlicedCheckBox.setOnCheckedChangeListener { _, isChecked ->
isEnableSlicedDownload = isChecked
}
}
private fun initButtonClickListeners() {
binding.startDownloadButton.setOnClickListener {
activityResultLauncher.launch(
arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
)
}
binding.pauseDownloadButton.setOnClickListener {
if (isDownloadManagerInitialized().not()) [email protected]
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PROCESS -> {
downloadManager.pauseRequest(getRequest.id)
}
else -> {
Toast.makeText(this, "No valid download request", Toast.LENGTH_SHORT).show()
}
}
}
binding.resumeDownloadButton.setOnClickListener {
if (isDownloadManagerInitialized().not()) [email protected]
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PAUSE -> {
downloadManager.resumeRequest(getRequest, fileRequestCallback)
}
else -> {
Toast.makeText(this, "No download process", Toast.LENGTH_SHORT).show()
}
}
}
binding.cancelDownloadButton.setOnClickListener {
if (isDownloadManagerInitialized().not()) [email protected]
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PROCESS -> {
downloadManager.cancelRequest(getRequest.id)
clearAllViews()
}
Result.STATUS.PAUSE -> {
downloadManager.cancelRequest(getRequest.id)
clearAllViews()
}
else -> {
Toast.makeText(this, "No valid download request", Toast.LENGTH_SHORT).show()
}
}
}
binding.pasteClipboardImageButton.setOnClickListener {
pasteClipboardData()
}
}
private val activityResultLauncher =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
)
{ permissions ->
val allGranted = permissions.entries.map {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
checkSelfPermission(it.key)
} else {
true
}
}.map { it == PackageManager.PERMISSION_GRANTED }.find { !it } ?: true
if (!allGranted) {
Toast.makeText(this, "Permission are not granted", Toast.LENGTH_SHORT).show()
} else {
startDownload()
}
}
private fun startDownload() {
if (this::downloadManager.isInitialized) {
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PAUSE -> {
Toast.makeText(
this,
"Press Resume Button to continue download process",
Toast.LENGTH_SHORT
).show()
return
}
Result.STATUS.PROCESS -> {
Toast.makeText(
this,
"First cancel the current download process",
Toast.LENGTH_SHORT
).show()
return
}
}
}
downloadManager = DownloadManager.Builder("downloadManager")
.build(this)
val fileName = downloadURL.substringAfterLast("/")
val downloadFilePath = this.cacheDir.path + File.separator + fileName
val currentDownloadURL = binding.urlTextInputEditText.text.toString()
getRequest = DownloadManager.newGetRequestBuilder()
.filePath(downloadFilePath)
.url(currentDownloadURL)
.speedLimit(downloadSpeedLimit)
.enableSlice(isEnableSlicedDownload)
.build()
val result = downloadManager.start(getRequest, fileRequestCallback)
if (result.code != Result.SUCCESS) {
Log.d(TAG, "An Error occurred when downloading")
}
}
private fun convertByteToMb(sizeInByte: Long): String? {
return if (sizeInByte < 0 || sizeInByte == 0L) {
null
} else {
val sizeInMb: Float = sizeInByte / (1024 * 1024).toFloat()
String.format("%.2f", sizeInMb)
}
}
private fun showCurrentDownloadSpeed(speedInByte: Long) {
val downloadSpeedText = if (speedInByte <= 0) {
"-"
} else {
val sizeInKb: Float = speedInByte / 1024.toFloat()
String.format("%.2f", sizeInKb) + "kB/s"
}
binding.currentSpeedTextView.text = downloadSpeedText
}
private fun calculateSpeedLimitAsByte(progressBarValue: Int): Int {
return when (progressBarValue) {
0 -> 512 * 1024
1 -> 1024 * 1024
2 -> 2 * 1024 * 1024
3 -> 4 * 1024 * 1024
4 -> 6 * 1024 * 1024
5 -> 8 * 1024 * 1024
6 -> 16 * 1024 * 1024
7 -> 0
else -> 0
}
}
private fun showDownloadSpeedLimit(progressValue: Int) {
val message = when (progressValue) {
0 -> "512 kB/s"
1 -> "1 mB/s"
2 -> "2 mB/s"
3 -> "4 mB/s"
4 -> "6 mB/s"
5 -> "8 mB/s"
6 -> "16 mB/s"
7 -> "Limitless"
else -> "Error"
}
binding.speedLimitTextView.text = message
}
private fun isDownloadManagerInitialized(): Boolean {
return if (this::downloadManager.isInitialized) {
true
} else {
Toast.makeText(this, "First start the download", Toast.LENGTH_SHORT).show()
false
}
}
private fun pasteClipboardData() {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = clipboardManager.primaryClip
val clipItem = clipData?.getItemAt(0)
val text = clipItem?.text.toString()
if (text == "null") {
Toast.makeText(this, "There is no text on clipboard", Toast.LENGTH_SHORT).show()
} else {
binding.urlTextInputEditText.setText(text)
}
}
private fun showRemainingTime(progress: Progress) {
val elapsedTime = System.nanoTime() - startTime
val allTimeForDownloading =
(elapsedTime * progress.totalSize / progress.finishedSize)
val remainingTime = allTimeForDownloading - elapsedTime
val hours = TimeUnit.NANOSECONDS.toHours(remainingTime)
val minutes = TimeUnit.NANOSECONDS.toMinutes(remainingTime) % 60
val seconds = TimeUnit.NANOSECONDS.toSeconds(remainingTime) % 60
val remainingTimeAsText = if (hours > 0) {
"${hours}h ${minutes}m ${seconds}s left"
} else {
if (minutes > 0) {
"${minutes}m ${seconds}s left"
} else {
"${seconds}s left"
}
}
binding.remainingTimeTextView.text = remainingTimeAsText
}
private fun showSnackBar(rootView: View, message: String) {
val snackBar = Snackbar.make(rootView, message, Snackbar.LENGTH_SHORT)
snackBar.show()
}
private fun clearAllViews() {
binding.percentProgressTextView.text = "0%"
binding.finishedSizeTextView.text = "0"
binding.totalSizeTextView.text = "0"
binding.currentSpeedTextView.text = "0 kB/s"
binding.downloadProgressProgressBar.progress = 0
binding.remainingTimeTextView.text = "0s left"
}
}
Tips & Tricks
According to the Wi-Fi status awareness capability of the Huawei Awareness Kit, you can pause or resume your download task. It will reduce the cost to the user and help to manage your download process properly.
Before starting the download task, you can check that you’re connected to the internet using the ConnectivityManager.
If the download file has the same name as an existing file, it will overwrite the existing file. Therefore, you should give different names for your files.
Even if you minimize the application, the download will continue in the background.
Conclusion
In this article, we have learned how to use Network Kit in your download tasks. And, we’ve developed the Download Manager app that provides many features. In addition to these features, you can also use Network Kit in your upload tasks. Please do not hesitate to ask your questions as a comment.
Thank you for your time and dedication. I hope it was helpful. See you in other articles.
References
Huawei Network Kit Official Documentation
Huawei Network Kit Official Codelab
Huawei Network Kit Official Github
Thanks for sharing.
Introduction
In this article, we can learn about the CRUD (Create, Read, Update and Delete) operations in Room Database in Attendance Tracker app.
Room is an ORM (Object Relational Mapping) library. In other words, Room will map our database objects to Java objects. Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. Room can provide to create sqlite databases and perform the operations like create, read, update and delete.
So, I will provide the series of articles on this Attendance Tracker App, in upcoming articles I will integrate other Huawei Kits.
If you are new to this application, follow my previous articles
Beginner: Integration of Huawei Account Kit of Obtaining Icon Resources feature in Attendance Tracker Android app (Kotlin) - Part 1
Beginner: Find Log in via SMS and Forgot password? Features using Huawei Account Kit, and Ads Kit in Attendance Tracker Android app (Kotlin) – Part 2
Components of Room DB
1. Entity
2. Dao
3. Database
1. Entity
Represents a table within the database. Room creates a table for each class that has @entity annotation, the fields in the class correspond to columns in the table. Therefore, the entity classes tend to be small model classes that does not contain any logic.
Entities annotations
Before we get started with modelling our entities, we need to know some useful annotations and their attributes.
@entity - every model class with this annotation will have a mapping table in DB.
foreignKeys — names of foreign keys
indices — list of indicates on the table
primaryKeys — names of entity primary keys
tableName
@primarykey - as name indicates, this annotation points is the primary key of the entity. autoGenerate - if set to true, then SQLite will be generating a unique id for the column.
Example: @PrimaryKey(autoGenerate = true)
@ColumnInfo - allows specifying custom information about column.
Example: @ColumnInfo(name = “column_name”)
@ignore - field will not be persisted by Room.
@embeded - nested fields can be referenced directly in the SQL queries.
2. Dao
DAOs(Data Access Objects) are responsible for defining the methods that access the database. In the initial SQLite, we use the Cursor objects. With Room, we do not need all the Cursor related code and can simply define our queries using annotations in the Dao class.
3. Database
Contains the database holder and serves as the main access point for the underlying connection to your app’s persisted, relational data.
To create a database, we need to define an abstract class that extends RoomDatabase. This class is annotated with @database, lists the entities contained in the database, and the DAOs which access them.
The class which is annotated with @database should satisfy the following conditions:
Be an abstract class that extends RoomDatabase.
Include the list of entities associated with the database within the annotation.
Contain an abstract method that has 0 arguments and returns the class that is annotated with @dao.
Requirements
1. Any operating system (MacOS, Linux and Windows).
2. Must have a Huawei phone with HMS 4.0.0.300 or later.
3. Must have a laptop or desktop with Android Studio, Jdk 1.8, SDK platform 26 and Gradle 4.6 and above installed.
4. Minimum API Level 24 is required.
5. Required EMUI 9.0.0 and later version devices.
How to integrate HMS Dependencies
1. First register as Huawei developer and complete identity verification in Huawei developers website, refer to register a Huawei ID.
2. Create a project in android studio, refer Creating an Android Studio Project.
3. Generate a SHA-256 certificate fingerprint.
4. To generate SHA-256 certificate fingerprint. On right-upper corner of android project click Gradle, choose Project Name > Tasks > android, and then click signingReport, as follows.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Note: Project Name depends on the user created name.
5. Create an App in AppGallery Connect.
6. Download the agconnect-services.json file from App information, copy and paste in android Project under app directory, as follows.
7. Enter SHA-256 certificate fingerprint and click Save button, as follows.
Note: Above steps from Step 1 to 7 is common for all Huawei Kits.
8. Add the below maven URL in build.gradle(Project) file under the repositories of buildscript, dependencies and allprojects, refer Add Configuration.
Java:
maven { url 'http://developer.huawei.com/repo/' }
classpath 'com.huawei.agconnect:agcp:1.6.0.300'
9. Add the below plugin and dependencies in build.gradle(Module) file.
Java:
apply plugin: id 'com.huawei.agconnect'
apply plugin: id 'kotlin-kapt'
// Huawei AGC
implementation 'com.huawei.agconnect:agconnect-core:1.6.0.300'
// Room Database
implementation "androidx.room:room-runtime:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
implementation "androidx.room:room-ktx:2.4.2"
androidTestImplementation "androidx.room:room-testing:2.4.2"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
// Recyclerview
implementation 'androidx.recyclerview:recyclerview:1.2.1'
10. Now Sync the gradle.
Let us move to development
I have created a project on Android studio with empty activity let us start coding.
Create a DataRecord.kt class annotated with @entity to create a table for each class.
Java:
@Entity(tableName = "datarecords")
data class DataRecord(
@PrimaryKey(autoGenerate = true)
val id: Long,
val name: String,
val age: String,
val phone: String
)
Create a DataRecordDao.kt interface class annotated with @dao and responsible for defining the methods that access the database.
Java:
@Dao
interface DataRecordDao {
@Query("SELECT * from datarecords")
fun getall(): LiveData<List<DataRecord>>
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(item: DataRecord)
@Query("SELECT * FROM datarecords WHERE datarecords.id == :id")
fun get(id: Long): LiveData<DataRecord>
@Update
suspend fun update(vararg items: DataRecord)
@Delete
suspend fun delete(vararg items: DataRecord)
}
Create a AppRoomDatabase.kt abstract class that extends RoomDatabase annotated with @database[/USER] to lists the entities contained in the database, and the DAOs which access them.
Java:
@Database(entities = [DataRecord::class], version = 1)
abstract class AppRoomDatabase : RoomDatabase() {
abstract fun datarecordDao(): DataRecordDao
companion object {
@Volatile
private var INSTANCE: RoomDatabase? = null
fun getDatabase(context: Context): AppRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance as AppRoomDatabase
}
synchronized(this) {
val instance = Room.databaseBuilder(context.applicationContext,AppRoomDatabase::class.java,
"datarecords_database").fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
Create a Repository.kt class to find the functions.
Java:
class Repository(private val dataDao: DataRecordDao) {
val allItems: LiveData<List<DataRecord>> = dataDao.getall()
fun get(id: Long):LiveData<DataRecord> {
return dataDao.get(id)
}
suspend fun update(item: DataRecord) {
dataDao.update(item)
}
suspend fun insert(item: DataRecord) {
dataDao.insert(item)
}
suspend fun delete(item: DataRecord) {
dataDao.delete(item)
}
}
Create a ViewModel.kt class that extends AndroidViewModel and provides the Repository functions.
Java:
class ViewModel(application: Application): AndroidViewModel(application) {
private val repository: Repository
val allItems: LiveData<List<DataRecord>>
init {
Log.d(TAG, "Inside ViewModel init")
val dao = AppRoomDatabase.getDatabase(application).datarecordDao()
repository = Repository(dao)
allItems = repository.allItems
}
fun insert(item: DataRecord) = viewModelScope.launch {
repository.insert(item)
}
fun update(item: DataRecord) = viewModelScope.launch {
repository.update(item)
}
fun delete(item: DataRecord) = viewModelScope.launch {
repository.delete(item)
}
fun get(id: Long) = repository.get(id)
}
In the EmployeesList.kt activity to find the business logic for button click.
Java:
class EmployeesList : AppCompatActivity() {
private var buttonAddEmp: Button? = null
private lateinit var myViewModel: ViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_employees_list)
val recyclerView = recyclerview_tasks
val adapter = EmpAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
buttonAddEmp = findViewById(R.id.btn_add)
buttonAddEmp!!.setOnClickListener(View.OnClickListener {
val intent = Intent(this, AddEmployees::class.java)
startActivity(intent)
})
myViewModel = ViewModelProvider(this)[ViewModel::class.java]
myViewModel.allItems.observe(this, Observer { items ->
items?.let { adapter.setItems(it) }
})
}
}
Create a EmpAdapter.kt adapter class to hold the list.
Java:
const val TAG = "EmpAdapter"
class EmpAdapter internal constructor (context: Context) : RecyclerView.Adapter<EmpAdapter.EmpRecordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var itemsList = emptyList<DataRecord>().toMutableList()
private val onClickListener: View.OnClickListener
init {
onClickListener = View.OnClickListener { v ->
val item = v.tag as DataRecord
Log.d(TAG, "Setting onClickListener for item ${item.id}")
val intent = Intent(v.context, AddEmployees::class.java).apply {
putExtra(DATA_RECORD_ID, item.id)
}
v.context.startActivity(intent)
}
}
inner class EmpRecordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val itemId: TextView = itemView.findViewById(R.id.datarecord_viewholder_id)
val itemName: TextView = itemView.findViewById(R.id.txt_name)
val itemAge: TextView = itemView.findViewById(R.id.txt_age)
val itemPhone: TextView = itemView.findViewById(R.id.txt_phone)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmpRecordViewHolder {
val itemView = inflater.inflate(R.layout.detail_list, parent, false)
return EmpRecordViewHolder(itemView)
}
override fun onBindViewHolder(holder: EmpRecordViewHolder, position: Int) {
val current = itemsList[position]
// It will be referenced in the View.OnClickListener above
holder.itemView.tag = current
with(holder) {
// Set UI values
itemId.text = current.id.toString()
// itemRecord.text = current.record
itemName.text = current.name
itemAge.text = current.age
itemPhone.text = current.phone
// Set handlers
itemView.setOnClickListener(onClickListener)
}
}
internal fun setItems(items: List<DataRecord>) {
this.itemsList = items.toMutableList()
notifyDataSetChanged()
}
override fun getItemCount() = itemsList.size
companion object {
const val DATA_RECORD_ID : String = "datarecord_id"
}
}
In the AddEmployees.kt activity to find the business logic create, update, read and delete functions.
Java:
class AddEmployees : AppCompatActivity() {
private lateinit var dataViewModel: ViewModel
private var recordId: Long = 0L
private var isEdit: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_employees)
dataViewModel = ViewModelProvider(this)[ViewModel::class.java]
if (intent.hasExtra(DATA_RECORD_ID)) {
recordId = intent.getLongExtra(DATA_RECORD_ID, 0L)
dataViewModel.get(recordId).observe(this, Observer {
val viewId = findViewById<TextView>(R.id.datarecord_id)
val viewName = findViewById<EditText>(R.id.edt_name)
val viewAge = findViewById<EditText>(R.id.edt_age)
val viewPhone = findViewById<EditText>(R.id.edt_phone)
if (it != null) {
// populate with data
viewId.text = it.id.toString()
viewName.setText(it.name)
viewAge.setText(it.age)
viewPhone.setText(it.phone)
}
})
isEdit = true
}
val save = btn_save
save.setOnClickListener { view ->
val id = 0L
val mName = edt_name.text.toString()
val mAge = edt_age.text.toString()
val mPhone = edt_phone.text.toString()
if (mName.isBlank() ) {
Toast.makeText(this, "Name is blank", Toast.LENGTH_SHORT).show()
}
else if (mAge.isBlank()) {
Toast.makeText(this, "Age is blank", Toast.LENGTH_SHORT).show()
}
else if(mPhone.isBlank()) {
Toast.makeText(this, "Phone is blank", Toast.LENGTH_SHORT).show()
}
else {
val item = DataRecord( id = id, name = mName, age = mAge, phone = mPhone)
dataViewModel.insert(item)
finish()
}
}
val update = btn_update
update.setOnClickListener { view ->
val id = datarecord_id.text.toString().toLong()
val nName = edt_name.text.toString()
val nAge = edt_age.text.toString()
val nPhone = edt_phone.text.toString()
if (nName.isBlank() && nAge.isBlank() && nPhone.isBlank()) {
Toast.makeText(this, "Empty data is not allowed", Toast.LENGTH_SHORT).show()
} else {
val item = DataRecord(id = id, name = nName, age = nAge, phone = nPhone)
dataViewModel.update(item)
finish()
}
}
val delete = btn_del
delete.setOnClickListener {
val id = datarecord_id.text.toString().toLong()
val nName = edt_name.text.toString()
val nAge = edt_age.text.toString()
val nPhone = edt_phone.text.toString()
val item = DataRecord( id =id, name = nName, age = nAge, phone = nPhone)
dataViewModel.delete(item)
finish()
}
}
}
In the activity_employees_list.xml we can create the UI screen.
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".RoomDatabase.EmployeesList">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:layout_gravity="center_horizontal"
android:text="Employees List"
android:textColor="#B602D5"
android:textSize="24sp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:padding="10dp"
android:layout_marginTop="40dp"
android:clickable="true" >
<ImageView
android:layout_width="100dp"
android:layout_height="50dp"
android:layout_marginRight="20dp"
android:drawableTint="@color/design_default_color_primary"
android:src="@drawable/add_emp"/>
<Button
android:id="@+id/btn_add"
android:layout_width="match_parent"
android:layout_height="50dp"
android:textSize="18dp"
android:textAllCaps="false"
android:text="Add Employees"/>
</LinearLayout>
</LinearLayout>
In the activity_add_employees.xml we can create the UI screen.
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".RoomDatabase.AddEmployees">
<TextView
android:id="@+id/datarecord_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="id "
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textSize="18sp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edt_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="Name "
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textSize="18sp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edt_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="Age "
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textSize="18sp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edt_phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="Phone "
android:textSize="18sp"
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textColor="@color/black" />
<Button
android:id="@+id/btn_save"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="5dp"
android:text="Save"/>
<Button
android:id="@+id/btn_update"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="5dp"
android:text="Update"/>
<Button
android:id="@+id/btn_del"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="5dp"
android:text="Delete"/>
</LinearLayout>
In the detail_list.xml we can create the UI screen for customized items.
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/datarecord_viewholder_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="10dp"
android:textSize="18sp"
android:text="id: "
android:textColor="@color/purple_500"
android:textStyle="bold" />
<TextView
android:id="@+id/txt_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/purple_500"
android:text="name: " />
<TextView
android:id="@+id/txt_age"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/purple_500"
android:text="age: " />
<TextView
android:id="@+id/txt_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/purple_500"
android:text="phone: " />
</LinearLayout>
</androidx.cardview.widget.CardView>
Demo
Tips and Tricks
1. Make sure you are already registered as Huawei developer.
2. Set minSDK version to 24 or later, otherwise you will get AndriodManifest merge issue.
3. Make sure you have added the agconnect-services.json file to app folder.
4. Make sure you have added SHA-256 fingerprint without fail.
5. Make sure all the dependencies are added properly.
Conclusion
In this article, we have learned the integration of Room database. We, have learnt about the room database and its components such as DAO, Entity and Database. How to create, read, update and delete the content in room database and which helps the user to access the data when they are in offline.
I hope you have read this article. If you found it is helpful, please provide likes and comments.
Reference
Room Database
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Introduction
In this article, we can learn about that how to enter In Time and Out time of an users by the Room Database and also we can integrate the Analytics Kit for event tracking.
Room is an ORM (Object Relational Mapping) library. In other words, Room will map our database objects to Java objects. Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. Room can provide to create sqlite databases and perform the operations like create, read, update and delete.
So, I will provide the series of articles on this Attendance Tracker App, in upcoming articles I will integrate other Huawei Kits.
If you are new to this application, follow my previous articles
Beginner: Integration of Huawei Account Kit of Obtaining Icon Resources feature in Attendance Tracker Android app (Kotlin) - Part 1
Beginner: Find Log in via SMS and Forgot password? Features using Huawei Account Kit, and Ads Kit in Attendance Tracker Android app (Kotlin) – Part 2
Beginner: Find the CRUD operations in Room Database in Attendance Tracker Android app (Kotlin) – Part 3
Beginner: Integration of Huawei Crash Service in Attendance Tracker Android app (Kotlin) – Part 4
Analytics Kit
HUAWEI Analytics Kit provides analysis models to understand user behaviour and gain in-depth insights into users, products and content. It helps you to gain insight about user behaviour on different platforms based on the user behaviour events and user attributes reported by through apps.
AppGallery Connect
Find the Analytics using AppGallery connect dashboard.
Choose My Projects > Huawei Analytics > Overview > Project overview.
Project overview displays the core indicators of current project, such as the number of new users, User activity, User acquisition, User revisit, New user retention, Active user retention, User characteristics and Popular pages etc. providing a quick overview of the users and how they are using your app.
Requirements
1. Any operating system (MacOS, Linux and Windows).
2. Must have a Huawei phone with HMS 4.0.0.300 or later.
3. Must have a laptop or desktop with Android Studio, Jdk 1.8, SDK platform 26 and Gradle 4.6 and above installed.
4. Minimum API Level 24 is required.
5. Required EMUI 9.0.0 and later version devices.
How to integrate HMS Dependencies
1. First register as Huawei developer and complete identity verification in Huawei developers website, refer to register a Huawei ID.
2. Create a project in android studio, refer Creating an Android Studio Project.
3. Generate a SHA-256 certificate fingerprint.
4. To generate SHA-256 certificate fingerprint. On right-upper corner of android project click Gradle, choose Project Name > Tasks > android, and then click signingReport, as follows.
Note: Project Name depends on the user created name.
5. Create an App in AppGallery Connect.
6. Download the agconnect-services.json file from App information, copy and paste in android Project under app directory, as follows.
7. Enter SHA-256 certificate fingerprint and click Save button, as follows.
Note: Above steps from Step 1 to 7 is common for all Huawei Kits.
8. Click Manage APIs tab and enable HUAWEI Analytics.
9. Add the below maven URL in build.gradle(Project) file under the repositories of buildscript, dependencies and allprojects, refer Add Configuration.
Java:
maven { url 'http://developer.huawei.com/repo/' }
classpath 'com.huawei.agconnect:agcp:1.6.0.300'
10. Add the below plugin and dependencies in build.gradle(Module) file.
Java:
apply plugin: id 'com.huawei.agconnect'
apply plugin: id 'kotlin-kapt'
// Huawei AGC
implementation 'com.huawei.agconnect:agconnect-core:1.6.0.300'
// Room Database
implementation "androidx.room:room-runtime:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
implementation "androidx.room:room-ktx:2.4.2"
androidTestImplementation "androidx.room:room-testing:2.4.2"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
// Recyclerview
implementation 'androidx.recyclerview:recyclerview:1.2.1'
// Huawei Analytics
implementation 'com.huawei.hms:hianalytics:6.4.0.300'
11. Now Sync the gradle.
12. Add the required permission to the AndroidManifest.xml file.
XML:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
Let us move to development
I have created a project on Android studio with empty activity let us start coding.
Initialize Huawei Analytics and enable it.
Java:
// Initialize the Analytics
var mInstance: HiAnalyticsInstance? = null
// Initialize the Analytics function
initAna()
private fun initAna() {
// Enable Analytics Kit Log
HiAnalyticsTools.enableLog()
// Generate the Analytics Instance
mInstance = HiAnalytics.getInstance(this)
// Enable collection capability
mInstance?.setAnalyticsEnabled(true)
}
Create a PunchRecord.kt class annotated with @entity to create a table for each class.
Java:
@Entity(tableName = "punchrecords")
data class PunchRecord(
@PrimaryKey(autoGenerate = true)
val id: Long,
val empid: String,
val intime: String,
val outtime: String
)
Create a PunchRecordDao.kt interface class annotated with @dao and responsible for defining the methods that access the database.
Code:
@Dao
interface PunchRecordDao {
@Query("SELECT * from punchrecords")
fun punchgetall(): LiveData<List<PunchRecord>>
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(item: PunchRecord)
@Query("SELECT * FROM punchrecords WHERE punchrecords.id == :id")
fun get(id: Long): LiveData<PunchRecord>
@Update
suspend fun update(vararg items: PunchRecord)
@Delete
suspend fun delete(vararg items: PunchRecord)
}
Create a AppRoomDatabase.kt abstract class that extends RoomDatabase annotated with @database to lists the entities contained in the database, and the DAOs which access them.
Code:
@Database(entities = [PunchRecord::class], version = 1)
abstract class PunchAppRoomDatabase : RoomDatabase() {
abstract fun punchrecordDao(): PunchRecordDao
companion object {
@Volatile
private var INSTANCE: RoomDatabase? = null
fun getDatabase(context: Context): PunchAppRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance as PunchAppRoomDatabase
}
synchronized(this) {
val instance = Room.databaseBuilder(context.applicationContext,PunchAppRoomDatabase::class.java,
"datarecords_database").fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
Create a PunchRepository.kt class to find the functions.
Code:
class PunchRepository(private val punchDao: PunchRecordDao) {
val myallItems: LiveData<List<PunchRecord>> = punchDao.punchgetall()
fun getrepo(id: Long):LiveData<PunchRecord> {
return punchDao.get(id)
}
suspend fun updatepunch(item: PunchRecord) {
punchDao.update(item)
}
suspend fun insertpunch(item: PunchRecord) {
punchDao.insert(item)
}
suspend fun deletepunch(item: PunchRecord) {
punchDao.delete(item)
}
}
Create a PunchViewModel.kt class that extends AndroidViewModel and provides the PunchRepository functions.
Java:
class PunchViewModel(application: Application): AndroidViewModel(application) {
private val repository: PunchRepository
val allPunchItems: LiveData<List<PunchRecord>>
init {
Log.d(ContentValues.TAG, "Inside ViewModel init")
val dao = PunchAppRoomDatabase.getDatabase(application).punchrecordDao()
repository = PunchRepository(dao)
allPunchItems = repository.myallItems
}
fun insert(item: PunchRecord) = viewModelScope.launch {
repository.insertpunch(item)
}
fun update(item: PunchRecord) = viewModelScope.launch {
repository.updatepunch(item)
}
fun delete(item: PunchRecord) = viewModelScope.launch {
repository.deletepunch(item)
}
fun get(id: Long) = repository.getrepo(id)
}
In the EmpLoginActivity.kt activity to find the business logic for button click.
Java:
class EmpLoginActivity : AppCompatActivity() {
private var buttonAddPunch: Button? = null
private lateinit var myViewModel: PunchViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_emp_login)
val recyclerView = recyclerview_list
val adapter = PunchAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
buttonAddPunch = findViewById(R.id.btn_punch)
buttonAddPunch!!.setOnClickListener(View.OnClickListener {
val intent = Intent(this, TimeActivity::class.java)
startActivity(intent)
})
myViewModel = ViewModelProvider(this)[PunchViewModel::class.java]
myViewModel.allPunchItems.observe(this, Observer { items ->
items?.let { adapter.setItems(it) }
})
}
}
Create a PunchAdapter.kt adapter class to hold the list.
Java:
class PunchAdapter internal constructor (context: Context) : RecyclerView.Adapter<PunchAdapter.PunchRecordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var itemsList = emptyList<PunchRecord>().toMutableList()
private val onClickListener: View.OnClickListener
init {
onClickListener = View.OnClickListener { v ->
val item = v.tag as PunchRecord
Log.d(TAG, "Setting onClickListener for item ${item.id}")
val intent = Intent(v.context, TimeActivity::class.java).apply {
putExtra(DATA_RECORD_ID, item.id)
}
v.context.startActivity(intent)
}
}
inner class PunchRecordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val itemId: TextView = itemView.findViewById(R.id.datarecord_viewholder_id)
val itemID: TextView = itemView.findViewById(R.id.punch_id)
val itemInTime: TextView = itemView.findViewById(R.id.punch_in)
val itemOutTime: TextView = itemView.findViewById(R.id.punch_out)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PunchRecordViewHolder {
val itemView = inflater.inflate(R.layout.punch_list, parent, false)
return PunchRecordViewHolder(itemView)
}
override fun onBindViewHolder(holder: PunchRecordViewHolder, position: Int) {
val current = itemsList[position]
// Needed: will be referenced in the View.OnClickListener above
holder.itemView.tag = current
with(holder) {
// Set UI values
itemId.text = current.id.toString()
// itemRecord.text = current.record
itemID.text = current.empid
itemInTime.text = current.intime
itemOutTime.text = current.outtime
// Set handlers
itemView.setOnClickListener(onClickListener)
}
}
internal fun setItems(items: List<PunchRecord>) {
this.itemsList = items.toMutableList()
notifyDataSetChanged()
}
override fun getItemCount() = itemsList.size
companion object {
const val DATA_RECORD_ID : String = "datarecord_id"
}
}
In the TimeActivity.kt activity to find the business logic to add In Time and Out Time.
Java:
class TimeActivity : AppCompatActivity(), DatePickerDialog.OnDateSetListener, TimePickerDialog.OnTimeSetListener {
private lateinit var dataViewModel: PunchViewModel
private var recordId: Long = 0L
private var isEdit: Boolean = false
var day = 0
var month = 0
var year = 0
var hour = 0
var minute = 0
var savedDay = 0
var savedMonth = 0
var savedYear = 0
var savedHour = 0
var savedMinute = 0
var isInTime : Boolean = false
var isOutTime : Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_time)
dataViewModel = ViewModelProvider(this)[PunchViewModel::class.java]
if (intent.hasExtra(PunchAdapter.DATA_RECORD_ID)) {
recordId = intent.getLongExtra(PunchAdapter.DATA_RECORD_ID, 0L)
dataViewModel.get(recordId).observe(this, Observer {
val viewId = findViewById<TextView>(R.id.datarecord_id)
val viewEmpid = findViewById<EditText>(R.id.edtpunch_id)
val viewInTime = findViewById<EditText>(R.id.edtpunch_intime)
val viewOutTime = findViewById<EditText>(R.id.edtpunch_outtime)
if (it != null) {
// populate with data
viewId.text = it.id.toString()
viewEmpid.setText(it.empid)
viewInTime.setText(it.intime)
viewOutTime.setText(it.outtime)
}
})
isEdit = true
}
edtpunch_intime.setOnClickListener{
isInTime = true
isOutTime = false
getDateTimeCalendar()
DatePickerDialog(this,this,year,month,day ).show()
}
edtpunch_outtime.setOnClickListener{
getDateTimeCalendar()
isInTime = false
isOutTime = true
DatePickerDialog(this,this,year, month, day ).show()
}
btnpunch_save.setOnClickListener { view ->
val id = 0L
val mEmpid = edtpunch_id.text.toString()
val mInTime = edtpunch_intime.text.toString()
val mOutTime = edtpunch_outtime.text.toString()
if (mEmpid.isBlank() ) {
Toast.makeText(this, "Employee ID is blank", Toast.LENGTH_SHORT).show()
}
else if (mInTime.isBlank()) {
Toast.makeText(this, "In Time is blank", Toast.LENGTH_SHORT).show()
}
else if(mOutTime.isBlank()) {
Toast.makeText(this, "Out Time is blank", Toast.LENGTH_SHORT).show()
}
else {
val item = PunchRecord(id = id, empid = mEmpid, intime = mInTime, outtime = mOutTime)
dataViewModel.insert(item)
finish()
}
}
btnpunch_update.setOnClickListener { view ->
val id = datarecord_id.text.toString().toLong()
val nEmpid = edtpunch_id.text.toString()
val nInTime = edtpunch_intime.text.toString()
val nOutTime = edtpunch_outtime.text.toString()
if (nEmpid.isBlank() && nInTime.isBlank() && nOutTime.isBlank()) {
Toast.makeText(this, "Empty data is not allowed", Toast.LENGTH_SHORT).show()
} else {
val item = PunchRecord(id = id, empid = nEmpid, intime = nInTime, outtime = nOutTime)
dataViewModel.update(item)
finish()
}
}
btnpunch_delete.setOnClickListener {
val id = datarecord_id.text.toString().toLong()
val nEmpid = edtpunch_id.text.toString()
val nInTime = edtpunch_intime.text.toString()
val nOutTime = edtpunch_outtime.text.toString()
val item = PunchRecord( id =id, empid = nEmpid, intime = nInTime, outtime = nOutTime)
dataViewModel.delete(item)
finish()
}
}
private fun getDateTimeCalendar(){
val cal: Calendar = Calendar.getInstance()
day = cal.get(Calendar.DAY_OF_MONTH)
month = cal.get(Calendar.MONTH)
year = cal.get(Calendar.YEAR)
hour = cal.get(Calendar.HOUR)
minute = cal.get(Calendar.MINUTE)
}
override fun onDateSet(view: DatePicker?, year: Int, month: Int, dayOfMonth: Int) {
savedDay = dayOfMonth
savedMonth = month
savedYear = year
getDateTimeCalendar()
TimePickerDialog(this, this, hour, minute, true).show()
}
override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) {
savedHour = hourOfDay
savedMinute = minute
if (isInTime) {
edtpunch_intime.setText("$savedDay-$savedMonth-$savedYear, $savedHour:$savedMinute")
}
if (isOutTime) {
edtpunch_outtime.setText("$savedDay-$savedMonth-$savedYear, $savedHour:$savedMinute")
}
}
}
In the activity_emp_login.xml we can create the UI screen.
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".RoomDatabase.EmpLoginActivity">
<Button
android:id="@+id/btn_punch"
android:layout_width="200dp"
android:layout_height="50dp"
android:textSize="18dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:layout_gravity="center_horizontal"
android:textAllCaps="false"
android:text="Click to Punch"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
In the activity_time.xml we can create the UI screen.
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".RoomDatabase.TimeActivity">
<TextView
android:id="@+id/datarecord_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="id "
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textSize="18sp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edtpunch_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="EmpID: "
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textSize="18sp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edtpunch_intime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="In Time: "
android:textSize="18sp"
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textColor="@color/black" />
<EditText
android:id="@+id/edtpunch_outtime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_gravity="left"
android:hint="Out Time: "
android:textSize="18sp"
android:padding="5dp"
android:layout_marginLeft="10dp"
android:textColor="@color/black" />
<Button
android:id="@+id/btnpunch_save"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="5dp"
android:text="Save"/>
<Button
android:id="@+id/btnpunch_update"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="5dp"
android:text="Update"/>
<Button
android:id="@+id/btnpunch_delete"
android:layout_width="150dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:textSize="18dp"
android:textAllCaps="false"
android:layout_marginTop="5dp"
android:text="Delete"/>
</LinearLayout>
In the punch_list.xml we can create the UI screen for customized items.
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/datarecord_viewholder_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="10dp"
android:textSize="18sp"
android:text="id: "
android:textColor="@color/black"
android:textStyle="bold" />
<TextView
android:id="@+id/punch_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/black"
android:text="ID: " />
<TextView
android:id="@+id/punch_in"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/black"
android:text="IN Time: " />
<TextView
android:id="@+id/punch_out"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/black"
android:text="OUT Time: " />
</LinearLayout>
</androidx.cardview.widget.CardView>
Demo
Tips and Tricks
1. Make sure you are already registered as Huawei developer.
2. Set minSDK version to 24 or later, otherwise you will get AndriodManifest merge issue.
3. Make sure you have added the agconnect-services.json file to app folder.
4. Make sure you have added SHA-256 fingerprint without fail.
5. Make sure all the dependencies are added properly.
Conclusion
In this article, we can learn about that how to enter In Time and Out time of an users by the Room Database and also, we can integrate the Analytics Kit for event tracking.
I hope you have read this article. If you found it is helpful, please provide likes and comments.
Reference
Analytics Kit - Documentation
Analytics Kit – Training Video
Room Database
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Introduction
In this article, we will learn how to connect smart gloves from the paired devices. The process is in the smart gloves application. Smart glove application shows all the paired Bluetooth devices. User has to select the gloves and connect to Bluetooth device.
If you are new to the series of articles, follow articles.
Beginner: Integration of Huawei Account kit in Navigation Glove IoT application Using Kotlin - Part 1
Beginner: Integration of Huawei Map kit in Navigation Glove IoT application Using Kotlin - Part 2
Beginner: Integration of Huawei Site kit in Navigation Glove IoT application Using Kotlin - Part 3
Beginner: Integration of Huawei Direction API in Navigation Glove IoT application Using Kotlin - Part 4
In this article, we learn how to display all the paired device in the application. We will also learn how to integrate the Recyclerview in android. Make sure device is already paired to phone.
Note: Application is built in androidx if you are developing application in the older than androidx you need to add the recyclerview dependencies.
Add the following permission in AndroidManifest.xml
XML:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
Follow the steps
Step 1: Add the recyclerview in the activity_connect_smart_gloves.xml
XML:
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/pairedDeviceBtn"
android:layout_below="@id/messageTv"
tools:listitem="@layout/paired_device_list_item"/>
Step 2: If you want to display items let us create one xml file.
paired_device_list_item.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_margin="10dp"
app:cardElevation="6dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="5dp"
android:id="@+id/rootLayout">
<ImageView
android:id="@+id/imageview"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="5dp"
android:src="@drawable/gloves_ic"
android:visibility="visible" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="5dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="15dp"
android:text="@string/name"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/deviceName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="15dp"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center|start"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="15dp"
android:text="@string/mac_address"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/macAddress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginLeft="15dp"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
Step 3: Create data class for device model (DeviceModel.kt)
Java:
package com.huawei.navigationglove.model
data class DeviceModel(val name: String, val macAddress: String)
Step 4: declare layout manager in activity.
Java:
private lateinit var linearLayoutManager: LinearLayoutManager
Step 5: Declare the device model variable and the paired device list.
Java:
private var pairedDeviceList = ArrayList<DeviceModel>()
var deviceModel: DeviceModel? = null
Step 6: Initialize the layout manager and set it to recyclerview in onCreate().
Java:
linearLayoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = linearLayoutManager
Step 7: Now create PairedDeviceListAdapter.kt for recycler view.
Java:
package com.huawei.navigationglove.ui.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.huawei.navigationglove.R
import com.huawei.navigationglove.callbacks.OnDeviceClickCallback
import com.huawei.navigationglove.model.DeviceModel
class PairedDevicesAdapter(
private val mList: List<DeviceModel>,
private val onDeviceClickCallback: OnDeviceClickCallback
) :
RecyclerView.Adapter<PairedDevicesAdapter.ViewHolder>() {
// create new views
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.paired_device_list_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val deviceModel = mList[position]
holder.deviceName.text = deviceModel.name
holder.macAddress.text = deviceModel.macAddress
holder.rootLayout.setOnClickListener { onDeviceClickCallback.onDeviceClick(deviceModel) }
}
// return the number of the items in the list
override fun getItemCount(): Int {
return mList.size
}
// Holds the views for adding it to image and text
class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) {
val deviceName: TextView = itemView.findViewById(R.id.deviceName)
val macAddress: TextView = itemView.findViewById(R.id.macAddress)
val rootLayout: LinearLayout = itemView.findViewById(R.id.rootLayout)
}
}
Step 8: Now get all the paired devices, and set the list of devices to adapter.
Java:
private fun pairedDevicesList() {
pairedDeviceList = ArrayList<DeviceModel>()
pairedDevices = myBluetooth!!.bondedDevices
val list: ArrayList<String> = ArrayList<String>()
if ((pairedDevices as MutableSet<BluetoothDevice>?)!!.size > 0) {
for (bt in (pairedDevices as MutableSet<BluetoothDevice>?)!!) {
Log.e(TAG, bt.name + "\n" + bt.address)
list.add(bt.name + "\n" + bt.address) //Get the device's name and the address
deviceModel = DeviceModel(bt.name, bt.address)
pairedDeviceList.add(deviceModel!!)
}
val adapter = PairedDevicesAdapter(pairedDeviceList, this)
recyclerView.adapter = adapter
} else {
Toast.makeText(
applicationContext,
"No Paired Bluetooth Devices Found.",
Toast.LENGTH_LONG
).show()
}
}
Step 9: By now recyclerview will show the list of paired devices. Now when user clicks on the device it has to connect. So now lets create onclick for recyclerview. To do that we will do it using interface callback.
Create OnDeviceClickCallback.kt interface.
Java:
package com.huawei.navigationglove.callbacks
import com.huawei.navigationglove.model.DeviceModel
interface OnDeviceClickCallback {
fun onDeviceClick(deviceModel: DeviceModel)
}
Step 9: Implement this interface in activity.
Java:
class ConnectSmartGlovesActivity : AppCompatActivity(), OnDeviceClickCallback {
Now override the function.
Java:
override fun onDeviceClick(deviceModel: DeviceModel) {
address = deviceModel.macAddress
ConnectBT(this).execute()
}
Step 10: Now create following variables.
Java:
private var progress: ProgressDialog? =null //This progress dialog box while connecting to bluetooth
private var isBtConnected = false
var btSocket: BluetoothSocket? = null //Bridge which sends data to bluetooth from Android app
var myBluetooth: BluetoothAdapter? = null
val myUUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB") //Unique
var address: String? = null
Step: 11: Create Async class for connecting.
//This class contains functions that need to be done to connect to the HC-05 module
Java:
open class ConnectBT(val context: Context) : AsyncTask<Void?, Void?, Void?>() // UI thread
{
private var ConnectSuccess = true //if it's here, it's almost connected
override fun onPreExecute() {
progress = ProgressDialog.show(
context,
"Connecting...",
"Please wait!!!"
) //show a progress dialog
}
@SuppressLint("MissingPermission")
override fun doInBackground(vararg devices: Void?): Void? //while the progress dialog is shown, the connection is done in background
{
try {
if (btSocket == null || !isBtConnected) {
myBluetooth =
BluetoothAdapter.getDefaultAdapter() //get the mobile bluetooth device
val dispositivo: BluetoothDevice = myBluetooth!!.getRemoteDevice(address) //connects to the device's address and checks if it's available
Log.e(TAG,"Device: "+Gson().toJson(dispositivo))
btSocket = dispositivo.createInsecureRfcommSocketToServiceRecord(myUUID) //create a RFCOMM (SPP) connection
Log.e(TAG,"Device: "+Gson().toJson(btSocket))
BluetoothAdapter.getDefaultAdapter().cancelDiscovery()
btSocket!!.connect() //start connection
}
} catch (e: IOException) {
ConnectSuccess = false //if the try failed, you can check the exception here
Log.e("MapsActivity","Device not connected" +e.message)
}
return null
}
override fun onPostExecute(result: Void?) //after the doInBackground, it checks if everything went fine
{
super.onPostExecute(result)
if (!ConnectSuccess) {
val toast =
Toast.makeText(context, "Failure", Toast.LENGTH_LONG)
toast.show()
val handler = Handler()
handler.postDelayed({ toast.cancel() }, 300)
//TODO need to check this
//finish()
} else {
val toast = Toast.makeText(context, "Success.", Toast.LENGTH_LONG)
toast.show()
val handler = Handler()
handler.postDelayed({ toast.cancel() }, 300)
//msg("Connected.");
SystemClock.sleep(1000)
isBtConnected = true
}
progress!!.dismiss()
}
}
Step 12: To get the connection and disconnection state create register broadcast receiver in onCreate().
Java:
val filter = IntentFilter()
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED)
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
this.registerReceiver(nReceiver, filter)
Step 13: Create broadcast receiver.
Java:
//This BroadcastReceiver finds listens the status of bluetooth
private val nReceiver: BroadcastReceiver? = object : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.M)
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
if (BluetoothDevice.ACTION_FOUND == action) {
} else if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
co = true
Log.e("mode:", "Connected")
requestLocationUpdatesWithCallback()
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED == action) {
} else if (BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action) {
Log.e("mode:", "disConnected")
} else if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action) {
co = false
Log.e("mode:", "disConnected1")
val vib = context.getSystemService(Vibrator::class.java)
Log.e("Vibe", "" + vib.hasVibrator())
//t1.speak("Your Bluetooth Has Been Disconnected", TextToSpeech.QUEUE_FLUSH, null);
for (i in 0..4) {
vib.vibrate(700)
SystemClock.sleep(1000)
}
i = 0
}
}
}
Result
Tips and Tricks
1. Make sure you have added Bluetooth permission in AndroidManifest.xml
2. Make sure you phone Bluetooth is enabled.
3. If you are building application below androidx, Make sure you have added the recyclerview dependency in build.gradle file.
Conclusion
In this article, we have learnt how to use recyclerview in the android application along with that we have displayed the list of paired devices in the smart gloves application. And also we have learnt how to connect device from the smart gloves application.
Reference
Bluetooth Official document
Recyclerview Document
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Introduction
In this article, we can learn how to integrate Roll Ads feature of Huawei Ads Kit into the android app. So, Roll ads are displayed as short videos or images, before, during, or after the video content is played.
Ads Kit
Huawei Ads provides to developers a wide-ranging capabilities to deliver good quality ads content to users. This is the best way to reach the target audience easily and can measure user productivity. It is very useful when we publish a free app and want to earn some money from it.
HMS Ads Kit has 7 types of Ads kits. Now we can implement Roll Ads in this application.
Requirements
1. Any operating system (MacOS, Linux and Windows).
2. Must have a Huawei phone with HMS 4.0.0.300 or later.
3. Must have a laptop or desktop with Android Studio, Jdk 1.8, SDK platform 26 and Gradle 4.6 and above installed.
4. Minimum API Level 24 is required.
5. Required EMUI 9.0.0 and later version devices.
How to integrate HMS Dependencies
1. First register as Huawei developer and complete identity verification in Huawei developers website, refer to register a Huawei ID.
2. Create a project in android studio, refer Creating an Android Studio Project.
3. Generate a SHA-256 certificate fingerprint.
4. To generate SHA-256 certificate fingerprint. On right-upper corner of android project click Gradle, choose Project Name > Tasks > android, and then click signingReport, as follows.
Note: Project Name depends on the user created name.
5. Create an App in AppGallery Connect.
6. Download the agconnect-services.json file from App information, copy and paste in android Project under app directory, as follows.
7. Enter SHA-256 certificate fingerprint and click Save button, as follows.
Note: Above steps from Step 1 to 7 is common for all Huawei Kits.
8. Add the below maven URL in build.gradle(Project) file under the repositories of buildscript, dependencies and allprojects, refer Add Configuration.
Java:
maven { url 'http://developer.huawei.com/repo/' }
classpath 'com.huawei.agconnect:agcp:1.6.0.300'
9. Add the below plugin and dependencies in build.gradle(Module) file.
Java:
apply plugin: id 'com.huawei.agconnect'
// Huawei AGC
implementation 'com.huawei.agconnect:agconnect-core:1.6.0.300'
// Huawei Ads Kit
implementation 'com.huawei.hms:ads-lite:13.4.51.300'
10. Now Sync the gradle.
11. Add the required permission to the AndroidManifest.xml file.
Java:
// Ads Kit
<uses-permission android:name="android.permission.INTERNET" />
Let us move to development
I have created a project on Android studio with empty activity let us start coding.
In the MainActivity.kt we can find the business logic for Ads.
Java:
class MainActivity : AppCompatActivity() {
private var videoContent: TextView? = null
private var skipAd: TextView? = null
private var countDown: TextView? = null
private var callToAction: TextView? = null
private var loadButton: Button? = null
private var registerButton: Button? = null
private var muteButton: Button? = null
private var pauseButton: Button? = null
private var instreamContainer: RelativeLayout? = null
private var instreamView: InstreamView? = null
private var whyThisAd: ImageView? = null
private var context: Context? = null
private var maxAdDuration = 0
private var whyThisAdUrl: String? = null
private var isMuted = false
private var adLoader: InstreamAdLoader? = null
private var instreamAds: List<InstreamAd>? = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
context = applicationContext
setTitle(R.string.instream_ad)
initInstreamAdView()
initButtons()
configAdLoader()
}
private val mediaChangeListener = InstreamMediaChangeListener { instreamAd ->
whyThisAdUrl = null
whyThisAdUrl = instreamAd.whyThisAd
Log.i(TAG, "onSegmentMediaChange, whyThisAd: $whyThisAdUrl")
if (!TextUtils.isEmpty(whyThisAdUrl)) {
whyThisAd!!.visibility = View.VISIBLE
whyThisAd!!.setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(whyThisAdUrl))) }
} else {
whyThisAd!!.visibility = View.GONE
}
val cta = instreamAd.callToAction
if (!TextUtils.isEmpty(cta)) {
callToAction!!.visibility = View.VISIBLE
callToAction!!.text = cta
instreamView!!.callToActionView = callToAction
}
}
private val mediaStateListener: InstreamMediaStateListener = object : InstreamMediaStateListener {
override fun onMediaProgress(per: Int, playTime: Int) {
updateCountDown(playTime.toLong())
}
override fun onMediaStart(playTime: Int) {
updateCountDown(playTime.toLong())
}
override fun onMediaPause(playTime: Int) {
updateCountDown(playTime.toLong())
}
override fun onMediaStop(playTime: Int) {
updateCountDown(playTime.toLong())
}
override fun onMediaCompletion(playTime: Int) {
updateCountDown(playTime.toLong())
playVideo()
}
override fun onMediaError(playTime: Int, errorCode: Int, extra: Int) {
updateCountDown(playTime.toLong())
}
}
private val mediaMuteListener: MediaMuteListener = object : MediaMuteListener {
override fun onMute() {
isMuted = true
Toast.makeText(context, "Ad muted", Toast.LENGTH_SHORT).show()
}
override fun onUnmute() {
isMuted = false
Toast.makeText(context, "Ad unmuted", Toast.LENGTH_SHORT).show()
}
}
private fun initInstreamAdView() {
instreamContainer = findViewById(R.id.instream_ad_container)
videoContent = findViewById(R.id.instream_video_content)
skipAd = findViewById(R.id.instream_skip)
skipAd!!.setOnClickListener(View.OnClickListener {
if (null != instreamView) {
instreamView!!.onClose()
instreamView!!.destroy()
instreamContainer!!.visibility = View.GONE
}
})
countDown = findViewById(R.id.instream_count_down)
callToAction = findViewById(R.id.instream_call_to_action)
whyThisAd = findViewById(R.id.instream_why_this_ad)
instreamView = findViewById(R.id.instream_view)
instreamView!!.setInstreamMediaChangeListener(mediaChangeListener)
instreamView!!.setInstreamMediaStateListener(mediaStateListener)
instreamView!!.setMediaMuteListener(mediaMuteListener)
instreamView!!.setOnInstreamAdClickListener(InstreamView.OnInstreamAdClickListener {
Toast.makeText(context,"instream clicked.", Toast.LENGTH_SHORT).show()
})
}
private val clickListener = View.OnClickListener { view ->
when (view.id) {
R.id.instream_load -> if (null != adLoader) {
loadButton!!.text = getString(R.string.instream_loading)
adLoader!!.loadAd(AdParam.Builder().build())
}
R.id.instream_register -> if (null == instreamAds || instreamAds!!.isEmpty()) {
playVideo()
} else {
playInstreamAds(instreamAds!!)
}
R.id.instream_mute -> if (isMuted) {
instreamView!!.unmute()
muteButton!!.text = getString(R.string.instream_mute)
} else {
instreamView!!.mute()
muteButton!!.text = getString(R.string.instream_unmute)
}
R.id.instream_pause_play -> if (instreamView!!.isPlaying) {
instreamView!!.pause()
pauseButton!!.text = getString(R.string.instream_play)
} else {
instreamView!!.play()
pauseButton!!.text = getString(R.string.instream_pause)
}
else -> {
}
}
}
private fun initButtons() {
loadButton = findViewById(R.id.instream_load)
registerButton = findViewById(R.id.instream_register)
muteButton = findViewById(R.id.instream_mute)
pauseButton = findViewById(R.id.instream_pause_play)
loadButton!!.setOnClickListener(clickListener)
registerButton!!.setOnClickListener(clickListener)
muteButton!!.setOnClickListener(clickListener)
pauseButton!!.setOnClickListener(clickListener)
}
private val instreamAdLoadListener: InstreamAdLoadListener = object : InstreamAdLoadListener {
override fun onAdLoaded(ads: MutableList<InstreamAd>) {
if (null == ads || ads.size == 0) {
playVideo()
return
}
val it = ads.iterator()
while (it.hasNext()) {
val ad = it.next()
if (ad.isExpired) {
it.remove()
}
}
if (ads.size == 0) {
playVideo()
return
}
loadButton!!.text = getString(R.string.instream_loaded)
instreamAds = ads
Toast.makeText(context, "onAdLoaded, ad size: " + ads.size + ", click REGISTER to play.", Toast.LENGTH_SHORT).show()
}
override fun onAdFailed(errorCode: Int) {
Log.w(TAG, "onAdFailed: $errorCode")
loadButton!!.text = getString(R.string.instream_load)
Toast.makeText(context, "onAdFailed: $errorCode", Toast.LENGTH_SHORT).show()
playVideo()
}
}
private fun configAdLoader() {
// if the maximum total duration is 60 seconds and the maximum number of roll ads is eight,
// at most four 15-second roll ads or two 30-second roll ads will be returned.
// If the maximum total duration is 120 seconds and the maximum number of roll ads is four,
// no more roll ads will be returned after whichever is reached.
val totalDuration = 60
val maxCount = 4
val builder = InstreamAdLoader.Builder(context, getString(R.string.instream_ad_id))
adLoader = builder.setTotalDuration(totalDuration)
.setMaxCount(maxCount)
.setInstreamAdLoadListener(instreamAdLoadListener)
.build()
}
// play your normal video content.
private fun playVideo() {
hideAdViews()
videoContent!!.setText(R.string.instream_normal_video_playing)
}
private fun hideAdViews() {
instreamContainer!!.visibility = View.GONE
}
private fun playInstreamAds(ads: List<InstreamAd>) {
maxAdDuration = getMaxInstreamDuration(ads)
instreamContainer!!.visibility = View.VISIBLE
loadButton!!.text = getString(R.string.instream_load)
instreamView!!.setInstreamAds(ads)
}
private fun updateCountDown(playTime: Long) {
val time = Math.round((maxAdDuration - playTime) / 1000.toFloat()).toString()
runOnUiThread { countDown!!.text = time + "s" }
}
private fun getMaxInstreamDuration(ads: List<InstreamAd>): Int {
var duration = 0
for (ad in ads) {
duration += ad.duration.toInt()
}
return duration
}
override fun onPause() {
super.onPause()
if (null != instreamView && instreamView!!.isPlaying) {
instreamView!!.pause()
pauseButton!!.text = getText(R.string.instream_play)
}
}
override fun onResume() {
super.onResume()
if (null != instreamView && !instreamView!!.isPlaying) {
instreamView!!.play()
pauseButton!!.text = getText(R.string.instream_pause)
}
}
override fun onDestroy() {
super.onDestroy()
if (null != instreamView) {
instreamView!!.removeInstreamMediaStateListener()
instreamView!!.removeInstreamMediaChangeListener()
instreamView!!.removeMediaMuteListener()
instreamView!!.destroy()
}
}
companion object {
private val TAG = MainActivity::class.java.simpleName
}
}
In the activity_main.xml we can create the UI screen.
Java:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:id="@+id/your_video_content"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@android:color/black">
<TextView
android:id="@+id/instream_video_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
android:layout_centerInParent="true"
android:text="Your video content"
android:textColor="@android:color/white"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/instream_ad_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<com.huawei.hms.ads.instreamad.InstreamView
android:id="@+id/instream_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/instream_skip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="3dp"
android:paddingHorizontal="5dp"
android:layout_alignStart="@id/instream_view"
android:layout_alignLeft="@id/instream_view"
android:layout_alignTop="@id/instream_view"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:text="Skip"
android:textColor="@android:color/white"
android:textSize="16sp"
android:background="@drawable/emui_button_select"/>
<TextView
android:id="@+id/instream_count_down"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="3dp"
android:paddingHorizontal="5dp"
android:layout_alignEnd="@id/instream_view"
android:layout_alignRight="@id/instream_view"
android:layout_alignTop="@id/instream_view"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:textColor="@android:color/white"
android:textSize="16sp"
android:background="@drawable/emui_button_select"/>
<TextView
android:id="@+id/instream_ad_flag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="@id/instream_view"
android:layout_alignLeft="@id/instream_view"
android:layout_alignBottom="@id/instream_view"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginBottom="8dp"
android:background="@drawable/emui_button_select"
android:gravity="center"
android:text="Ad"
android:textColor="@android:color/white"
android:textSize="8sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/instream_why_this_ad"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_toEndOf="@id/instream_ad_flag"
android:layout_toRightOf="@id/instream_ad_flag"
android:layout_alignBottom="@id/instream_view"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:src="@drawable/app_whythisad_info"/>
<TextView
android:id="@+id/instream_call_to_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="3dp"
android:paddingHorizontal="5dp"
android:layout_alignEnd="@id/instream_view"
android:layout_alignRight="@id/instream_view"
android:layout_alignBottom="@id/instream_view"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="8dp"
android:text="Learn more"
android:textColor="@android:color/white"
android:textSize="16sp"
android:background="@drawable/emui_button_select"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/instream_ctrl_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:layout_below="@id/your_video_content">
<LinearLayout
android:id="@+id/load_and_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_centerHorizontal="true">
<Button
android:id="@+id/instream_load"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:text="Load Ad"/>
<Button
android:id="@+id/instream_register"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:text="Register"/>
</LinearLayout>
<LinearLayout
android:id="@+id/play_ctrl"
android:layout_below="@id/load_and_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_centerHorizontal="true">
<Button
android:id="@+id/instream_mute"
android:layout_width="100dp"
android:layout_marginRight="10dp"
android:layout_height="wrap_content"
android:text="Mute"/>
<Button
android:id="@+id/instream_pause_play"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="Pause"/>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
</ScrollView>
Demo
Tips and Tricks
1. Make sure you are already registered as Huawei developer.
2. Set minSDK version to 24 or later, otherwise you will get AndriodManifest merge issue.
3. Make sure you have added the agconnect-services.json file to app folder.
4. Make sure you have added SHA-256 fingerprint without fail.
5. Make sure all the dependencies are added properly.
Conclusion
In this article, we have learned how to integrate Roll Ads feature of Huawei Ads Kit into the android app. So, Roll ads are displayed as short videos or images, before, during, or after the video content is played.
I hope you have read this article. If you found it is helpful, please provide likes and comments.
Reference
Ads Kit - Roll Ads
Ads Kit – Training Video