Getting Latest Corona News with Huawei Search Kit

berkberber

Member
Jun 30, 2020
15
4
3
1 ysk4kz7bdTmdJSaD2wRFLg.png


Huawei Search Kit includes device-side SDK and cloud-side APIs to use all features of Petal Search capabilities. It helps developers to integrate mobile app search experience into their application.

Huawei Search Kit offers to developers so much different and helpful features. It decreases our development cost with SDKs and APIs, it returns responses quickly and it helps us to develop our application faster.

As a developer, we have some responsibilities and function restrictions while using Huawei Search Kit. If you would like to learn about these responsibilities and function restrictions, I recommend you to visit following website.
https://developer.huawei.com/consum.../HMSCore-Guides/introduction-0000001055591730

Also, Huawei Search Kit supports limited countries and regions. If you wonder about these countries and regions, you can visit the following website.
https://developer.huawei.com/consum...HMSCore-Guides-V5/regions-0000001056871703-V5

How to use Huawei Search Kit?
First of all, we need to create an app on AppGallery Connect and add related details about HMS Core to our project.
If you don’t know about how to integrate HMS Core to our project, you can learn all details from following Medium article.

https://medium.com/huawei-developers/android-integrating-your-apps-with-huawei-hms-core-1f1e2a090e98

After we have done all steps in above Medium article, we can focus on special steps of integrating Huawei Search Kit.
  • Our minSdkVersion should be 24 at minimum.
  • We need to add following dependency to our app level build.gradle file.
Code:
implementation "com.huawei.hms:searchkit:5.0.4.303"
  • Then, we need to do some changes on AppGallery Connect. We need to define a data storage location on AppGallery Connect.
    Note: If we don’t define a data storage location, all responses will return null.
1 yj09yXXLVtV-FBiU3_PBOQ.png


  • We need to initialize the SearchKit instance on our application which we have extended from android.app.Application class. To initialize the SearchKit instance, we need to set the app id on second parameter which has mentioned as Constants.APP_ID.
    While adding our application class to AndroidManifest.xml file, we need to set android:usesCleartextTraffic as true. You can do all these steps as mentioned in red rectangles.
1 nftUhMJJjlWyAMKaLBamDQ.png


Getting Access Token
For each request on Search Kit, we need to use access token. I prefer to get this access token on splash screen of the application. Thus, we will be able to save access token and save it with SharedPreferences.

First of all, we need to create our methods and objects about network operations. I am using Koin Framework for dependency injection on this project.
For creating objects about network operations, I have created following single objects and methods.
Note: In above picture, I have initialized the koin framework and added network module. Check this step to use this module in the app.
Java:
val networkModule = module {
    single { getOkHttpClient(androidContext()) }
    single { getRetrofit(get()) }
    single { getService<AccessTokenService>(get()) }
}

fun getRetrofit(okHttpClient: OkHttpClient): Retrofit {
    return Retrofit.Builder().baseUrl("https://oauth-login.cloud.huawei.com/")
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
}

fun getOkHttpClient(context: Context): OkHttpClient {
    return OkHttpClient().newBuilder()
        .sslSocketFactory(SecureSSLSocketFactory.getInstance(context), SecureX509TrustManager(context))
        .hostnameVerifier(StrictHostnameVerifier())
        .readTimeout(10, TimeUnit.SECONDS)
        .connectTimeout(1, TimeUnit.SECONDS)
        .retryOnConnectionFailure(true)
        .build()
}

inline fun <reified T> getService(retrofit: Retrofit): T = retrofit.create(T::class.java)
We have defined methods to create OkHttpClient and Retrofit objects. These objects have used as single to create Singleton objects. Also, we have defined one generic method to use Retrofit with our services.

To get an access token, our base URL will be “https://oauth-login.cloud.huawei.com/".

To get response from access token request, we need to define an object for response. The best way to do that is creating data class which is as shown in the below.

Java:
data class AccessTokenResponse(
    @SerializedName("access_token") val accessToken: String?,
    @SerializedName("expires_in") val expiresIn: Int?,
    @SerializedName("token_type") val tokenType: String?
)

Now, all we need to do is, creating an interface to send requests with Retrofit. To get access token, our total URL is “https://oauth-login.cloud.huawei.com/oauth2/v3/token". We need to send 3 parameters as x-www-form-url encoded. Let’s examine these parameters.
  • grant_type: This parameter will not change depends on our application. Value should be, “client_credentials”.
  • client_id: This parameter will be app id of our project.
  • client_secret: This parameter will be app secret of our project.
Java:
interface AccessTokenService {
    @FormUrlEncoded
    @POST("oauth2/v3/token")
    fun getAccessToken(
        @Field("grant_type") grantType: String,
        @Field("client_id") appId: String,
        @Field("client_secret") clientSecret: String
    ): Call<AccessTokenResponse>
}
Now, everything is ready to get an access token. We just need to send the request and save the access token with SharedPreferences.
To work with SharedPreferences, I have created a helper class as shown in the below.
Java:
class CacheHelper {
    companion object {
        private lateinit var instance: CacheHelper
        private var gson: Gson = Gson()

        private const val PREFERENCES_NAME = BuildConfig.APPLICATION_ID
        private const val PREFERENCES_MODE = AppCompatActivity.MODE_PRIVATE

        fun getInstance(context: Context): CacheHelper {
            instance = CacheHelper(context)
            return instance
        }
    }

    private var context: Context
    private var sharedPreferences: SharedPreferences
    private var sharedPreferencesEditor: SharedPreferences.Editor

    private constructor(context: Context) {
        this.context = context
        sharedPreferences = this.context.getSharedPreferences(PREFERENCES_NAME, PREFERENCES_MODE)
        sharedPreferencesEditor = sharedPreferences.edit()
    }

    fun putObject(key: String, `object`: Any) {
        sharedPreferencesEditor.apply {
            putString(key, gson.toJson(`object`))
            commit()
        }
    }

    fun <T> getObject(key: String, `object`: Class<T>): T? {
        return sharedPreferences.getString(key, null)?.let {
            gson.fromJson(it, `object`)
        } ?: kotlin.run {
            null
        }
    }
}
With the help of this class, we will be able to work with SharedPreferences easier.
Now, all we need to do it, sending request and getting access token.

Java:
object SearchKitService: KoinComponent {
    private val accessTokenService: AccessTokenService by inject()
    private val cacheHelper: CacheHelper by inject()

    fun initAccessToken(requestListener: IRequestListener<Boolean, Boolean>) {
        accessTokenService.getAccessToken(
            "client_credentials",
            Constants.APP_ID,
            Constants.APP_SECRET
        ).enqueue(object: retrofit2.Callback<AccessTokenResponse> {
            override fun onResponse(call: Call<AccessTokenResponse>, response: Response<AccessTokenResponse>) {
                response.body()?.accessToken?.let { accessToken ->
                    cacheHelper.putObject(Constants.ACCESS_TOKEN_KEY, accessToken)
                    requestListener.onSuccess(true)
                } ?: kotlin.run {
                    requestListener.onError(true)
                }
            }

            override fun onFailure(call: Call<AccessTokenResponse>, t: Throwable) {
                requestListener.onError(false)
            }

        })
    }
}
If API returns as access token successfully, we will save this access token to device using SharedPreferences. And on our SplashFragment, we need to listen IRequestListener and if onSuccess method returns true, that means we got the access token successfully and we can navigate application to BrowserFragment.

Huawei Search Kit
In this article, I will give examples about News Search, Image Search and Video Search features of Huawei Search Kit.

In this article, I will give examples about News Search, Image Search and Video Search features of Huawei Search Kit.
To send requests for News Search, Image Search and Video Search, we need a CommonSearchRequest object.
In this app, I will get results about Corona in English. I have created the following method to return to CommonSearchRequest object.

Java:
private fun returnCommonRequest(): CommonSearchRequest {
    return CommonSearchRequest().apply {
        setQ("Corona Virus")
        setLang(Language.ENGLISH)
        setSregion(Region.WHOLEWORLD)
        setPs(20)
        setPn(1)
    }
}

Here, we have setted some informations. Let’s examine this setter methods.
  • setQ(): Setting the keyword for search.
  • setLang(): Setting the language for search. Search Kit has it’s own model for language. If you would like examine this enum and learn about which Languages are supporting by Search Kit, you can visit the following website.
    Huawei Search Kit — Language Model
  • setSregion(): Setting the region for search. Search Kit has it’s own model for region. If you would like examine this enum and learn about which Regions are supporting by Search Kit, you can visit the following website.
    Huawei Search Kit — Region Model
  • setPn(): Setting the number about how much items will be in current page. The value ranges from 1 to 100, and the default value is 1.
  • setPs(): Setting the number of search results that will be returned on a page. The value ranges from 1 to 100, and the default value is 10.

Now, all we need to do is getting news, images, videos and show the results for these on the screen.

News Search
To get news, we can use the following method.

Java:
fun newsSearch(requestListener: IRequestListener<List<NewsItem>, String>) {
    SearchKitInstance.getInstance().newsSearcher.setCredential(SearchKitService.accessToken)
    var newsList = SearchKitInstance.getInstance().newsSearcher.search(SearchKitService.returnCommonRequest())
    newsList?.getData()?.let { newsItems ->
        requestListener.onSuccess(newsItems)
    } ?: kotlin.run {
        requestListener.onError("No value returned")
    }
}
1 zB61rr2Lh4XNSQqKfJxTfg.jpeg


Image Search
To get images, we can use the following method.

Java:
fun imageSearch(requestListener: IRequestListener<List<ImageItem>, String>) {
    SearchKitInstance.getInstance().imageSearcher.setCredential(SearchKitService.accessToken)
    var imageList = SearchKitInstance.getInstance().imageSearcher.search(SearchKitService.returnCommonRequest())
    imageList?.getData()?.let { imageItems ->
        requestListener.onSuccess(imageItems)
    } ?: kotlin.run {
        requestListener.onError("No value returned")
    }
}
1 0f8PMg-cRMx0huUQnKl90g.jpeg


Video Search
To get images, we can use the following method.

Java:
fun videoSearch(requestListener: IRequestListener<List<VideoItem>, String>) {
    SearchKitInstance.getInstance().videoSearcher.setCredential(SearchKitService.accessToken)
    var videoList = SearchKitInstance.getInstance().videoSearcher.search(SearchKitService.returnCommonRequest())
    videoList?.getData()?.let { videoList ->
        requestListener.onSuccess(videoList)
    } ?: kotlin.run {
        requestListener.onError("No value returned")
    }
}
1 hYSOi6ef4NzujsZNU6CHvg.jpeg


Showing on screen
All these results return a clickable url for each one. We can create an intent to open these URLs on the browser which has installed to device before.

To do that and other operations, I will share BrowserFragment codes for fragment and the SearchItemAdapter codes for recyclerview.

Java:
class BrowserFragment: Fragment() {
    private lateinit var viewBinding: FragmentBrowserBinding

    private lateinit var searchOptionsTextViews: ArrayList<TextView>

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        viewBinding = FragmentBrowserBinding.inflate(inflater, container, false)

        searchOptionsTextViews = arrayListOf(viewBinding.news, viewBinding.images, viewBinding.videos)

        return viewBinding.root
    }

    private fun setListeners() {
        viewBinding.news.setOnClickListener { getNews() }
        viewBinding.images.setOnClickListener { getImages() }
        viewBinding.videos.setOnClickListener { getVideos() }
    }

    private fun getNews() {
        SearchKitService.newsSearch(object: IRequestListener<List<NewsItem>, String>{
            override fun onSuccess(newsItemList: List<NewsItem>) {
                setupRecyclerView(newsItemList, viewBinding.news)
            }

            override fun onError(errorMessage: String) {
                Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
            }
        })
    }

    private fun getImages(){
        SearchKitService.imageSearch(object: IRequestListener<List<ImageItem>, String>{
            override fun onSuccess(imageItemList: List<ImageItem>) {
                setupRecyclerView(imageItemList, viewBinding.images)
            }

            override fun onError(errorMessage: String) {
                Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
            }
        })
    }

    private fun getVideos() {
        SearchKitService.videoSearch(object: IRequestListener<List<VideoItem>, String>{
            override fun onSuccess(videoItemList: List<VideoItem>) {
                setupRecyclerView(videoItemList, viewBinding.videos)
            }

            override fun onError(errorMessage: String) {
                Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
            }
        })
    }

    private val clickListener = object: IClickListener<String> {
        override fun onClick(clickedInfo: String) {
            var intent = Intent(Intent.ACTION_VIEW).apply {
                data = Uri.parse(clickedInfo)
            }
            startActivity(intent)
        }

    }

    private fun <T> setupRecyclerView(itemList: List<T>, selectedSearchOption: TextView) {
        viewBinding.searchKitRecyclerView.apply {
            layoutManager = LinearLayoutManager(requireContext())
            adapter = SearchItemAdapter<T>(itemList, clickListener)
        }

        changeSelectedTextUi(selectedSearchOption)
    }

    private fun changeSelectedTextUi(selectedSearchOption: TextView) {
        for (textView in searchOptionsTextViews)
            if (textView == selectedSearchOption) {
                textView.background = requireContext().getDrawable(R.drawable.selected_text)
            } else {
                textView.background = requireContext().getDrawable(R.drawable.unselected_text)
            }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setListeners()
        getNews()
    }
}
Java:
class SearchItemAdapter<T>(private val searchItemList: List<T>,
                           private val clickListener: IClickListener<String>):
    RecyclerView.Adapter<SearchItemAdapter.SearchItemHolder<T>>(){

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchItemHolder<T> {
        val itemBinding = ItemSearchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return SearchItemHolder<T>(itemBinding)
    }

    override fun onBindViewHolder(holder: SearchItemHolder<T>, position: Int) {
        val item = searchItemList[position]
        var isLast = (position == searchItemList.size - 1)
        holder.bind(item, isLast, clickListener)
    }

    override fun getItemCount(): Int = searchItemList.size

    override fun getItemViewType(position: Int): Int = position

    class SearchItemHolder<T>(private val itemBinding: ItemSearchBinding): RecyclerView.ViewHolder(itemBinding.root) {
        fun bind(item: T, isLast: Boolean, clickListener: IClickListener<String>) {
            if (isLast)
                itemBinding.itemSeparator.visibility = View.GONE
            lateinit var clickUrl: String
            var imageUrl = "https://www.who.int/images/default-source/infographics/who-emblem.png?sfvrsn=877bb56a_2"
            when(item){
                is NewsItem -> {
                    itemBinding.searchResultTitle.text = item.title
                    itemBinding.searchResultDetail.text = item.provider.siteName
                    clickUrl = item.clickUrl
                    item.provider.logo?.let { imageUrl = it }
                }
                is ImageItem -> {
                    itemBinding.searchResultTitle.text = item.title
                    clickUrl = item.clickUrl
                    item.sourceImage.image_content_url?.let { imageUrl = it }
                }
                is VideoItem -> {
                    itemBinding.searchResultTitle.text = item.title
                    itemBinding.searchResultDetail.text = item.provider.siteName
                    clickUrl = item.clickUrl
                    item.provider.logo?.let { imageUrl = it }
                }
            }

            itemBinding.searchItemRoot.setOnClickListener {
                clickListener.onClick(clickUrl)
            }
            getImageFromUrl(imageUrl, itemBinding.searchResultImage)
        }

        private fun getImageFromUrl(url: String, imageView: ImageView) {
            Glide.with(itemBinding.root)
                .load(url)
                .centerCrop()
                .into(imageView);
        }
    }
}
End
If you would like to learn more about Search Kit and see the Codelab, you can visit the following websites:

https://developer.huawei.com/consum.../HMSCore-Guides/introduction-0000001055591730

https://developer.huawei.com/consumer/en/codelab/HMSSearchKit/index.html#0
 

Attachments

  • Like
Reactions: VD171