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"
}
Hello everyone,
In this article, I would like to tell you about Cloud DB, which online storage that Huawei offers to developers.
What is Cloud DB ?
Although Cloud DB is still in beta version, it is a successful and seamless database structure. In addition to the ease of use, attracts developers with its management and a user-friendly interface. In addition to providing data availability, consistency and security, CloudDB provides seamless data synchronization between the device and the cloud.
If you do not have a server when developing applications, Cloud DB server easily solves our data storage, maintenance and distribution. Also CloudDB is free.
Cloud DB provides 500 GB of data volume for each application, and supports 2000 connections. Looking at their counterparts, it is understood how large and how important these numbers are.
Cloud DB Structure
Object Type: It represents each table in the standard database. In other words, each table containing data and columns is called Object Type.
Cloud DB Zone : Represents the data zone on the cloud side. According to the classic database, Cloud DB Zone is the name of the database or schema name.
Data Entires : It is the area that shows the added data. Here, you can add, update and delete data. When you add data, you will realize that the tables you are used to are in the same way. And while using this technology, it prevents you from suffering from strangeness.
How To Using Cloud DB ?
Now let’s see how to use Cloud DB. Since Cloud DB is still in beta, you have to send a mail to activate this service in your app. To use Cloud DB, you need to create an app after creating your Huawei Developer account. After completing all the necessary steps, you have to request the activation of the service by sending an e-mail to [email protected] with the sample header below. The service will be activated within 1–3 business days after your mail arrives. And you can start using it freely.
Cloud DB –Company Name — Developer ID — App ID
After your service activated, log in to AppGallery Connect and select your app under the heading “My Apps”. You can then access the “Cloud DB” panel under the “Build” tab from the menu on the left side of the page, by moving to the “Develop” tab in the upper left. After the page is loaded, activate the service by clicking the “Enable Now” button in the upper right corner.
A Cloud DB Zone must first be created. After then object types should be created. When creating the object type, column names must be entered and the primary key must be identified in the window that opens. Also, at this step, you can edit access control options for the object type. In the image of the below you can find, which users have which permissions should be given. After creating DB Zone and Object Type, Cloud DB provides to export data models and Helper class as JSON or Java for use them in the app without wasting time. After exporting the model classes as Java, you have to add these classes to the relevant directory of the app and start communicate with Cloud DB.
Cloud DB library should be added to the build.gradle file under the project’s app directory and the compatibility mode of the Java source code should be set as 1.8. For this, the following codes should be added to the gradle file and wait downloading the necessary dependencies by click Sync Now.
Code:
dependencies {
implementation 'com.huawei.agconnect:agconnect-database:1.2.1.301'
}
compileOptions {
targetCompatibility = 1.8
}
Let’s create a class named CloudDBZoneWrapper for all database operations. By defining all the upsert, query operations in this class, call these methods in the activity/fragment to be used. Thanks to this metods you can coding your app without clutter.
Firstly, Cloud DB objects should created to be used in this class.
Code:
private AGConnectCloudDB mCloudDB;
private CloudDBZone mCloudDBZone;
private ListenerHandler mRegister;
private CloudDBZoneConfig mConfig;
After then create an instance from the AGConnectCloudDB object in a constructor method.
Code:
public CloudDBZoneWrapper() {
mCloudDB = AGConnectCloudDB.getInstance();
}
Then, initAGConnectCloudDB method must be created for calling on the app landing page. This method must be run before the app is opened, before starting all DB operations. I follow the code by adding a log in every process step to follow the errors more easily. In this way, you can easily find out which line is wrong.
Code:
public static void initAGConnectCloudDB(Context context) {
AGConnectCloudDB.initialize(context);
Log.w(Constants.DB_ZONE_WRAPPER, "initAGConnectCloudDB" );
}
Next, creating Object Type, and open/close DBZone operations should be coding. These methods will be used before upsert and query operations for open DBZone and create Object Types.
Code:
public void createObjectType() {
try {
mCloudDB.createObjectType(ObjectTypeInfoHelper.getObjectTypeInfo());
Log.w(Constants.DB_ZONE_WRAPPER, "createObjectTypeSuccess " );
} catch (AGConnectCloudDBException e) {
Log.w(Constants.DB_ZONE_WRAPPER, "createObjectTypeError: " + e.getMessage());
}
}
public void openCloudDBZone() {
mConfig = new CloudDBZoneConfig("DB ZONE NAME HERE",
CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE,
CloudDBZoneConfig.CloudDBZoneAccessProperty.CLOUDDBZONE_PUBLIC);
mConfig.setPersistenceEnabled(true);
Log.w(Constants.DB_ZONE_WRAPPER, "openCloudDBZoneSuccess " );
try {
mCloudDBZone = mCloudDB.openCloudDBZone(mConfig, true);
} catch (AGConnectCloudDBException e) {
Log.w(Constants.DB_ZONE_WRAPPER, "openCloudDBZoneError: " + e.getMessage());
}
}
public void closeCloudDBZone() {
try {
mCloudDB.closeCloudDBZone(mCloudDBZone);
Log.w(Constants.DB_ZONE_WRAPPER, "closeCloudDBZoneSuccess " );
} catch (AGConnectCloudDBException e) {
Log.w(Constants.DB_ZONE_WRAPPER, "closeCloudDBZoneError: " + e.getMessage());
}
}
Now, the necessary methods for upsert and query operations should be written. But first, a few callbacks have to be added to get the results of these actions to the activities and fragments where the actions are operated. In this way, all DB operations will be gathered in a single class, without the activity being too tired and without crowd of code.
Code:
public interface UiCallBack {
void onAddOrQuery(List<TableUser> userList);
void isLastID(int lastID);.
void isDataUpsert(Boolean state);
}
public void addCallBacks(UiCallBack uiCallBack) {
mUiCallBack = uiCallBack;
}
Now the necessary method for upsert operation should be written. Upsert contains both insert and update operations. If upsert with a certain ID, the data in the related line will update. If it is upsert by new ID, a new line will added. So, both insert and update are carried out with the same method.
First, it should be checked whether DBZone is created or not. If DBZone has an error, will not upsert data. Then, upsert with CloudDBZoneTask object. Since I will add data to the User table, I gave the user object as a parameter to this method. If you need to add data to other tables, you should create a new method and give the object of the related table as a parameter. When upsert operation complated , if upsert is successful, it must return true, if occured an error, return false. For this, at the beginning of the method, a variable named Boolean state was defined and its first value was set as false. Then, if upsert is successful, state is set to true, and if error occurs, method will return false.
Code:
public void insertUser(TableUser user) {
boolean state = false;
if (mCloudDBZone == null) {
Log.w(Constants.DB_ZONE_WRAPPER, "INSERT USER : CloudDBZone is null, try re-open it");
return;
}
CloudDBZoneTask<Integer> upsertTask = mCloudDBZone.executeUpsert(user);
if (mUiCallBack == null) {
return;
}
upsertTask.addOnSuccessListener(new OnSuccessListener<Integer>() {
@Override
public void onSuccess(Integer cloudDBZoneResult) {
state = true;
Log.w(Constants.DB_ZONE_WRAPPER, "INSERT USER : upsert " + cloudDBZoneResult + " records");
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
state = false;
mUiCallBack.updateUiOnError("INSERT USER : Insert user info failed");
}
});
if (mUiCallBack != null) {
mUiCallBack.isDataUpsert(state);
}
}
Now, let’s make a query in the this class. For this, I will get the list of users which I added to the database with the same model class. Two methods are required when making query request. The first is the getAllUsers method for DB operations, and the other is called userListResult method, for add data to array. Firstly, CloudDBZone control should be done in getAllUsers method. Then, query request will make by creating a task. If the request is successful, the userListResult method is calling with the user object. If the request is successful, the userListResult method will called with the user object. An arrayList is created in the userListResult method, and all results are thrown into this list. Then, by adding a callback, the results can be called up in the activity or fragment.
Code:
public void getAllUsers() {
if (mCloudDBZone == null) {
Log.w(Constants.DB_ZONE_WRAPPER, "GET USER DETAIL : CloudDBZone is null, try re-open it");
return;
}
CloudDBZoneTask<CloudDBZoneSnapshot<TableUser>> queryTask = mCloudDBZone.executeQuery(
CloudDBZoneQuery.where(TableUser.class),
CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY);
queryTask.addOnSuccessListener(new OnSuccessListener<CloudDBZoneSnapshot<TableUser>>() {
@Override
public void onSuccess(CloudDBZoneSnapshot<TableUser> snapshot) {
userListResult (snapshot);
Log.w(Constants.DB_ZONE_WRAPPER, "GET USER DETAIL : GoResults: ");
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (mUiCallBack != null) {
mUiCallBack.updateUiOnError("GET USER DETAIL : Query user list from cloud failed");
}
}
});
}
private void userListResult (CloudDBZoneSnapshot<TableUser> snapshot) {
CloudDBZoneObjectList<TableUser> userInfoCursor = snapshot.getSnapshotObjects();
List<TableUser> userInfoList = new ArrayList<>();
try {
while (userInfoCursor.hasNext()) {
TableUser userInfo = userInfoCursor.next();
userInfoList.add(userInfo);
Log.w(Constants.DB_ZONE_WRAPPER, "USER DETAIL RESULT : processQueryResult: " + userInfo.getUser_city());
}
} catch (AGConnectCloudDBException e) {
Log.w(Constants.DB_ZONE_WRAPPER, "USER DETAIL RESULT : processQueryResult: " + e.getMessage());
}
snapshot.release();
if (mUiCallBack != null) {
mUiCallBack.onAddOrQuery(userInfoList);
}
}
Thus, all database operations within the CloudDBZoneWrapper class have been completed. Now let’s examine how to data upsert or query in activity or fragment.
The UiCallBack method in the CloudDBZoneWrapper class should be implement as the CloudDBZoneWrapper.UiCallBack in the class you which want to do database operations. In this way, all added call back methods will override in this class. Then the CloduDBZoneWrapper object and a new Handler need to be created in the activity. The CloudDBZoneWrapper object must be called within the constructor method. Sample codes are as follows.
Code:
private MyHandler mHandler = new MyHandler();
private CloudDBZoneWrapper mCloudDBZoneWrapper;
private static final class MyHandler extends Handler {
@Override
public void handleMessage(@NonNull Message msg) {
// dummy
}
}
public ProfileFragment() {
mCloudDBZoneWrapper = new CloudDBZoneWrapper();
}
Then the object type must be created in onCreate and Cloud DB Zone must be opened. If query is not related to an event, if the data should be loaded while the page is opening, call the getAllUsers method after creating the object type and opening the DB Zone in onCreate.
Code:
mHandler.post(() -> {
mCloudDBZoneWrapper.addCallBacks(ProfileFragment.this);
mCloudDBZoneWrapper.createObjectType();
mCloudDBZoneWrapper.openCloudDBZone();
mCloudDBZoneWrapper.getAllUsers();
});
The callback method, which was added to the getAllUsers method in the CloudDBZoneWrapper class, was override in the fragment. In this override method, all data can be used. For example, if wants to access the information of a user with ID = 3 in the user list, the turned list by insert a for loop, user information with ID = 3 is obtained.
Code:
@Override
public void onAddOrQuery(List<TableUser> userList) {
for(int i = 0; i <= userList.size()-1; i++){
if(userList.get(i).getId().equals(“3”)){
userName = userList.get(i).getUser_name());
userPhone = userList.get(i).getUser_phone();
userMail = userList.get(i).getUser_mail();
userAge = userList.get(i).getUser_age();
userGender = userList.get(i).getUser_gender();
}
}
}
Now let’s make an upsert. As I writed before, Upsert includes update and insert operations. Both operations are uses the same method. If you want to update a row of data, you must post with the ID information of the data in that row. If a new data is to be added, it should be posted with a new ID.
At this point, Cloud DB has a lack. Unfortunately, the auto increment don’t have when creating the object type. In other words, ID value does not increase automatically when data is added. It has to be given manually. I solved this problem by getting the last ID in the table and increasing it.
Now, create a method called updateUser to update and send back the user information I have previously got in this fragment. Next, a new user object should created here and the values should set. If there is an not to be changed data (as in the example, age and gender ), old values must be set in them. Finally, make post request by calling the insertUser method created in the CloudDBZoneWrapper class.
Code:
public void updateProfile(){
TableUser user = new TableUser();
user.setUser_id(“3”);
user.setUser_name(“Yeni İsim”));
user.setUser_phone(“Yeni Telefon”);
user.setUser_mail(“Yeni Mail”);
user.setUser_age(userAge);
user.setUser_gender(userGender);
mHandler.post(() -> {
mCloudDBZoneWrapper.insertUser(user);
});
}
In the fragment, help from call back methods should be obtained to find out whether the transaction is successful. Status check can be done in call back method added to insertUser method.
Code:
@Override
public void isDataUpsert(Boolean state) {
if(state){
//successful
}else{
//unsuccessful
}
}
Finally, it is worth mentioning that there is an authentication requirement to upsert. Since Cloud DB is still in beta, absolutely has some errors. But as you can see, all of them are solved easily. For authentication, Auth Service offered by Huawei to developers should be used. A service that is very easy to use. You can find the Auth Service link on the below. After the authentication, your upsert will work. If authentication is not done, the result of upsert will return false.
"https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-auth-service-introduction"
Well explained, can we store large amount of data into cloud Db, is there any limitations.
sujith.e said:
Well explained, can we store large amount of data into cloud Db, is there any limitations.
Click to expand...
Click to collapse
Yes, Cloud DB has a limit but I think you can store large data in Cloud DB. Because Cloud DB provides 500 GB of data volume for each application, and supports 2000 connections.
Introduction
Hello reader, in this article, I am going to demonstrate how to utilize Huawei Mobile Services (HMS) Search Kit to search for news articles from the web with customizable parameters. Also, I will show you how to use tools like auto suggestions and spellcheck capabilities provided by HMS Search Kit.
Getting Started
First, we need to follow instructions on the official website to integrate Search Kit into our app.
Getting Started
After we’re done with that, let’s start coding. First, we need to initialize Search Kit in our Application/Activity.
Java:
@HiltAndroidApp
class NewsApp : Application() {
override fun onCreate() {
super.onCreate()
SearchKitInstance.init(this, YOUR_APP_ID)
}
}
Next, let’s not forget adding our Application class to manifest. Also to allow HTTP network requests on devices with targetSdkVersion 28 or later, we need to allow clear text traffic. (Search Kit doesn’t support minSdkVersion below 24).
XML:
<application
android:name=".NewsApp"
android:usesCleartextTraffic="true">
...
</application>
Acquiring Access Token
The token is used to verify a search request on the server. Search results of the request are returned only after the verification is successful. Therefore, before we implement any search functions, we need to get the Access Token first.
OAuth 2.0-based Authentication
If you scroll down, you will see a method called Client Credentials, which does not require authorization from a user. In this mode, your app can generate an access token to access Huawei public app-level APIs. Exactly what we need. I have used Retrofit to do this job.
Let’s create a data class that represents the token response from Huawei servers.
Java:
data class TokenResponse(val access_token: String, val expires_in: Int, val token_type: String)
Then, let’s create an interface like below to generate Retrofit Service.
Java:
interface TokenRequestService {
@FormUrlEncoded
@POST("oauth2/v3/token")
suspend fun getRequestToken(
@Field("grant_type") grantType: String,
@Field("client_id") clientId: String,
@Field("client_secret") clientSecret: String
): TokenResponse
}
Then, let’s create a repository class to call our API service.
Java:
class NewsRepository(
private val tokenRequestService: TokenRequestService
) {
suspend fun getRequestToken() = tokenRequestService.getRequestToken(
"client_credentials",
YOUR_APP_ID,
YOUR_APP_SECRET
)
}
You can find your App ID and App secret from console.
{
"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"
}
I have used Dagger Hilt to provide Repository for view models that need it. Here is the Repository Module class that creates the objects to be injected to view models.
Java:
@InstallIn(SingletonComponent::class)
@Module
class RepositoryModule {
@Provides
@Singleton
fun provideRepository(
tokenRequestService: TokenRequestService
): NewsRepository {
return NewsRepository(tokenRequestService)
}
@Provides
@Singleton
fun providesOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().build()
}
@Provides
@Singleton
fun providesRetrofitClientForTokenRequest(okHttpClient: OkHttpClient): TokenRequestService {
val baseUrl = "https://oauth-login.cloud.huawei.com/"
return Retrofit.Builder()
.baseUrl(baseUrl)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
.create(TokenRequestService::class.java)
}
}
In order to inject our module, we need to add @HiltAndroidApp annotation to NewsApp application class. Also, add @AndroidEntryPoint to fragments that need dependency injection. Now we can use our repository in our view models.
I have created a splash fragment to get access token, because without it, none of the search functionalities would work.
Java:
@AndroidEntryPoint
class SplashFragment : Fragment(R.layout.fragment_splash) {
private var _binding: FragmentSplashBinding? = null
private val binding get() = _binding!!
private val viewModel: SplashViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentSplashBinding.bind(view)
lifecycleScope.launch {
viewModel.accessToken.collect {
if (it is TokenState.Success) {
findNavController().navigate(R.id.action_splashFragment_to_homeFragment)
}
if (it is TokenState.Failure) {
binding.progressBar.visibility = View.GONE
binding.tv.text = "An error occurred, check your connection"
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Java:
class SplashViewModel @ViewModelInject constructor(private val repository: NewsRepository) :
ViewModel() {
private var _accessToken = MutableStateFlow<TokenState>(TokenState.Loading)
var accessToken: StateFlow<TokenState> = _accessToken
init {
getRequestToken()
}
private fun getRequestToken() {
viewModelScope.launch {
try {
val token = repository.getRequestToken().access_token
SearchKitInstance.getInstance()
.setInstanceCredential(token)
SearchKitInstance.instance.newsSearcher.setTimeOut(5000)
Log.d(
TAG,
"SearchKitInstance.instance.setInstanceCredential done $token"
)
_accessToken.emit(TokenState.Success(token))
} catch (e: Exception) {
Log.e(HomeViewModel.TAG, "get token error", e)
_accessToken.emit(TokenState.Failure(e))
}
}
}
companion object {
const val TAG = "SplashViewModel"
}
}
As you can see, once we receive our access token, we call setInstanceCredential() method with the token as the parameter. Also I have set a 5 second timeout for the News Searcher. Then, Splash Fragment should react to the change in access token flow, navigate the app to the home fragment while popping splash fragment from back stack, because we don’t want to go back there. But if token request fails, the fragment will show an error message.
Setting up Search Kit Functions
Since we have given Search Kit the token it requires, we can proceed with the rest. Let’s add three more function to our repository.
1. getNews()
This function will take two parameters — search term, and page which will be used for pagination. NewsState is a sealed class that represents two states of news search request, success or failure.
Search Kit functions are synchronous, therefore we launch them in in the Dispatchers.IO context so they don’t block our UI.
In order to start a search request, we create an CommonSearchRequest, then apply our search parameters. setQ to set search term, setLang to set in which language we want to get our news (I have selected English), setSregion to set from which region we want to get our news (I have selected whole world), setPs to set how many news we want in single page, setPn to set which page of news we want to get.
Then we call the search() method to get a response from the server. if it is successful, we get a result in the type of BaseSearchResponse<List<NewsItem>>. If it’s unsuccessful (for example there is no network connection) we get null in return. In that case It returns failure state.
Java:
class NewsRepository(
private val tokenRequestService: TokenRequestService
) {
...
suspend fun getNews(query: String, pageNumber: Int): NewsState = withContext(Dispatchers.IO) {
var newsState: NewsState
Log.i(TAG, "getting news $query $pageNumber")
val commonSearchRequest = CommonSearchRequest()
commonSearchRequest.setQ(query)
commonSearchRequest.setLang(Language.ENGLISH)
commonSearchRequest.setSregion(Region.WHOLEWORLD)
commonSearchRequest.setPs(10)
commonSearchRequest.setPn(pageNumber)
try {
val result = SearchKitInstance.instance.newsSearcher.search(commonSearchRequest)
newsState = if (result != null) {
if (result.data.size > 0) {
Log.i(TAG, "got news ${result.data.size}")
NewsState.Success(result.data)
} else {
NewsState.Error(Exception("no more news"))
}
} else {
NewsState.Error(Exception("fetch news error"))
}
} catch (e: Exception) {
newsState = NewsState.Error(e)
Log.e(TAG, "caught news search exception", e)
}
[email protected] newsState
}
suspend fun getAutoSuggestions(str: String): AutoSuggestionsState =
withContext(Dispatchers.IO) {
val autoSuggestionsState: AutoSuggestionsState
autoSuggestionsState = try {
val result = SearchKitInstance.instance.searchHelper.suggest(str, Language.ENGLISH)
if (result != null) {
AutoSuggestionsState.Success(result.suggestions)
} else {
AutoSuggestionsState.Failure(Exception("fetch suggestions error"))
}
} catch (e: Exception) {
AutoSuggestionsState.Failure(e)
}
[email protected] autoSuggestionsState
}
suspend fun getSpellCheck(str: String): SpellCheckState = withContext(Dispatchers.IO) {
val spellCheckState: SpellCheckState
spellCheckState = try {
val result = SearchKitInstance.instance.searchHelper.spellCheck(str, Language.ENGLISH)
if (result != null) {
SpellCheckState.Success(result)
} else {
SpellCheckState.Failure(Exception("fetch spellcheck error"))
}
} catch (
e: Exception
) {
SpellCheckState.Failure(e)
}
[email protected] spellCheckState
}
companion object {
const val TAG = "NewsRepository"
}
}
2. getAutoSuggestions()
Search Kit can provide search suggestions with SearchHelper.suggest() method. It takes two parameters, a String to provide suggestions for, and a language type. If the operation is successful, a result in the type AutoSuggestResponse. We can access a list of SuggestObject from suggestions field of this AutoSuggestResponse. Every SuggestObject represents a suggestion from HMS which contains a String value.
3. getSpellCheck()
It works pretty much the same with auto suggestions. SearchHelper.spellCheck() method takes the same two parameters like suggest() method. But it returns a SpellCheckResponse, which has two important fields: correctedQuery and confidence. correctedQuery is what Search Kit thinks the corrected spelling should be, confidence is how confident Search kit is about the recommendation. Confidence has 3 values, which are 0 (not confident, we should not rely on it), 1 (confident), 2 (highly confident).
Using the functions above in our app
Home Fragments has nothing to show when it launches, because nothing has been searched yet. User can click the magnifier icon in toolbar to navigate to Search Fragment. Code for Search Fragment/View Model is below.
Notes:
Search View should expand on default with keyboard showing so user can start typing right away.
Every time query text changes, it will be emitted to a flow in view model. then it will be collected by two listeners in the fragment, first one to search for auto suggestions, second one to spell check. I did this to avoid unnecessary network calls, debounce(500) will make sure subsequent entries when the user is typing fast (less than half a second for a character) will be ignored and only the last search query will be used.
When user submit query term, the string will be sent back to HomeFragment using setFragmentResult() (which is only available fragment-ktx library Fragment 1.3.0-alpha04 and above).
Java:
@AndroidEntryPoint
class SearchFragment : Fragment(R.layout.fragment_search) {
private var _binding: FragmentSearchBinding? = null
private val binding get() = _binding!!
private val viewModel: SearchViewModel by viewModels()
@FlowPreview
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentSearchBinding.bind(view)
(activity as AppCompatActivity).setSupportActionBar(binding.toolbar)
setHasOptionsMenu(true)
//listen to the change in query text, trigger getSuggestions function after debouncing and filtering
lifecycleScope.launch {
viewModel.searchQuery.debounce(500).filter { s: String ->
[email protected] s.length > 3
}.distinctUntilChanged().flatMapLatest { query ->
Log.d(TAG, "getting suggestions for term: $query")
viewModel.getSuggestions(query).catch {
}
}.flowOn(Dispatchers.Default).collect {
if (it is AutoSuggestionsState.Success) {
val list = it.data
Log.d(TAG, "${list.size} suggestion")
binding.chipGroup.removeAllViews()
//create a chip for each suggestion and add them to chip group
list.forEach { suggestion ->
val chip = Chip(requireContext())
chip.text = suggestion.name
chip.isClickable = true
chip.setOnClickListener {
//set fragment result to return search term to home fragment.
setFragmentResult(
"requestKey",
bundleOf("bundleKey" to suggestion.name)
)
findNavController().popBackStack()
}
binding.chipGroup.addView(chip)
}
} else if (it is AutoSuggestionsState.Failure) {
Log.e(TAG, "suggestions request error", it.exception)
}
}
}
//listen to the change in query text, trigger spellcheck function after debouncing and filtering
lifecycleScope.launch {
viewModel.searchQuery.debounce(500).filter { s: String ->
[email protected] s.length > 3
}.distinctUntilChanged().flatMapLatest { query ->
Log.d(TAG, "spellcheck for term: $query")
viewModel.getSpellCheck(query).catch {
Log.e(TAG, "spellcheck request error", it)
}
}.flowOn(Dispatchers.Default).collect {
if (it is SpellCheckState.Success) {
val spellCheckResponse = it.data
val correctedStr = spellCheckResponse.correctedQuery
val confidence = spellCheckResponse.confidence
Log.d(
TAG,
"corrected query $correctedStr confidence level $confidence"
)
if (confidence > 0) {
//show spellcheck layout, and set on click listener to send corrected term to home fragment
//to be searched
binding.tvDidYouMeanToSearch.visibility = View.VISIBLE
binding.tvCorrected.visibility = View.VISIBLE
binding.tvCorrected.text = correctedStr
binding.llSpellcheck.setOnClickListener {
setFragmentResult(
"requestKey",
bundleOf("bundleKey" to correctedStr)
)
findNavController().popBackStack()
}
} else {
binding.tvDidYouMeanToSearch.visibility = View.GONE
binding.tvCorrected.visibility = View.GONE
}
} else if (it is SpellCheckState.Failure) {
Log.e(TAG, "spellcheck request error", it.exception)
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_search, menu)
val searchMenuItem = menu.findItem(R.id.searchItem)
val searchView = searchMenuItem.actionView as SearchView
searchView.setIconifiedByDefault(false)
searchMenuItem.expandActionView()
searchMenuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
findNavController().popBackStack()
return true
}
})
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return if (query != null && query.length > 3) {
setFragmentResult("requestKey", bundleOf("bundleKey" to query))
findNavController().popBackStack()
true
} else {
Toast.makeText(requireContext(), "Search term is too short", Toast.LENGTH_SHORT)
.show()
true
}
}
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.emitNewTextToSearchQueryFlow(newText ?: "")
return true
}
})
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
const val TAG = "SearchFragment"
}
}
Java:
class SearchViewModel @ViewModelInject constructor(private val repository: NewsRepository) :
ViewModel() {
private var _searchQuery = MutableStateFlow<String>("")
var searchQuery: StateFlow<String> = _searchQuery
fun getSuggestions(str: String): Flow<AutoSuggestionsState> {
return flow {
try {
val result = repository.getAutoSuggestions(str)
emit(result)
} catch (e: Exception) {
}
}
}
fun getSpellCheck(str: String): Flow<SpellCheckState> {
return flow {
try {
val result = repository.getSpellCheck(str)
emit(result)
} catch (e: Exception) {
}
}
}
fun emitNewTextToSearchQueryFlow(str: String) {
viewModelScope.launch {
_searchQuery.emit(str)
}
}
}
Now the HomeFragment has a search term to search for.
When the view is created, we receive the search term returned from Search Fragment on setFragmentResultListener. Then search for news using this query, then submit the PagingData to the recycler view adapter. Also, I made sure same flow will be returned if the new query is the same with the previous one so no unnecessary calls will be made.
Java:
@AndroidEntryPoint
class HomeFragment : Fragment(R.layout.fragment_home) {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeViewModel by viewModels()
private lateinit var listAdapter: NewsAdapter
private var startedLoading = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentHomeBinding.bind(view)
(activity as AppCompatActivity).setSupportActionBar(binding.toolbar)
setHasOptionsMenu(true)
listAdapter = NewsAdapter(NewsAdapter.NewsComparator, onItemClicked)
binding.rv.adapter =
listAdapter.withLoadStateFooter(NewsLoadStateAdapter(listAdapter))
//if user swipe down to refresh, refresh paging adapter
binding.swipeRefreshLayout.setOnRefreshListener {
listAdapter.refresh()
}
// Listen to search term returned from Search Fragment
setFragmentResultListener("requestKey") { _, bundle ->
// We use a String here, but any type that can be put in a Bundle is supported
val result = bundle.getString("bundleKey")
binding.tv.visibility = View.GONE
if (result != null) {
binding.toolbar.subtitle = "News about $result"
lifecycleScope.launchWhenResumed {
binding.swipeRefreshLayout.isRefreshing = true
viewModel.searchNews(result).collectLatest { value: PagingData<NewsItem> ->
listAdapter.submitData(value)
}
}
}
}
//need to listen to paging adapter load state to stop swipe to refresh layout animation
//if load state contain error, show a toast.
listAdapter.addLoadStateListener {
if (it.refresh is LoadState.NotLoading && startedLoading) {
binding.swipeRefreshLayout.isRefreshing = false
} else if (it.refresh is LoadState.Error && startedLoading) {
binding.swipeRefreshLayout.isRefreshing = false
val loadState = it.refresh as LoadState.Error
val errorMsg = loadState.error.localizedMessage
Toast.makeText(requireContext(), errorMsg, Toast.LENGTH_SHORT).show()
} else if (it.refresh is LoadState.Loading) {
startedLoading = true
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_home, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.searchItem -> {
//launch search fragment when search item clicked
findNavController().navigate(R.id.action_homeFragment_to_searchFragment)
true
}
else ->
super.onOptionsItemSelected(item)
}
}
//callback function to be passed to paging adapter, used to launch news links.
private val onItemClicked = { it: NewsItem ->
val builder = CustomTabsIntent.Builder()
val customTabsIntent = builder.build()
customTabsIntent.launchUrl(requireContext(), Uri.parse(it.clickUrl))
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
const val TAG = "HomeFragment"
}
}
Java:
class HomeViewModel @ViewModelInject constructor(private val repository: NewsRepository) :
ViewModel() {
private var lastSearchQuery: String? = null
var lastFlow: Flow<PagingData<NewsItem>>? = null
fun searchNews(query: String): Flow<PagingData<NewsItem>> {
return if (query != lastSearchQuery) {
lastSearchQuery = query
lastFlow = Pager(PagingConfig(pageSize = 10)) {
NewsPagingDataSource(repository, query)
}.flow.cachedIn(viewModelScope)
lastFlow as Flow<PagingData<NewsItem>>
} else {
lastFlow!!
}
}
companion object {
const val TAG = "HomeViewModel"
}
}
The app also uses Paging 3 library to provide endless scrolling for news articles, which is out of scope for this article, you may check the GitHub repo for how to achieve pagination with Search Kit. The end result looks like the images below.
Check the repo here.
Tips
When Search Kit fails to fetch results (example: no internet connection), it will return null object, you can manually return an exception so you can handle the error.
Conclusion
HMS Search Kit provide easy to use APIs for fast and efficient customizable searching for web sites, images, videos and news articles in many languages and regions. Also, it provides convenient features like auto suggestions and spellchecking.
Reference
Huawei Search Kit
What other features search kit provides other than news?
any additional feature can be supported?
Can we search daily base news ?
ask011 said:
What other features search kit provides other than news?
Click to expand...
Click to collapse
Hello, can reference documentation at https://developer.huawei.com/consum.../HMSCore-Guides/introduction-0000001055591730
ask011 said:
What other features search kit provides other than news?
Click to expand...
Click to collapse
Hello, can reference documentation at https://developer.huawei.com/consum.../HMSCore-Guides/introduction-0000001055591730
{
"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"
}
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.
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.
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")
}
}
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")
}
}
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")
}
}
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
Very nice guide.
Amazing.
Thank you very much
It's a very nice example.
Introduction
This article. we will learn how to implement the Card Ability — Huawei Account binding — solution 1 step by step.
Account Binding
Account binding refers to the process of binding a user’s HUAWEI ID and a developer’s account when developing card ability. Huawei provides three solutions to developers for account binding. We will work on solution-1 in this article.
if everybody is ready, let’s get started
Implementation
The account binding process consists of six phases in total. The picture below will help you better understand the process of account binding.
{
"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"
}
1.Enables Custom Sign Page — Account Binding Solution-1
Firstly, the developer server is mandatory for using solution-1. Huawei Ability Gallery sends the bind request to the developer’s server when custom sign mode is enabled from the Huawei phone.
2. Sends the Request to Obtain a Deep Link URL
Code:
@PostMapping("/api/v1/users/bind")
public AccountBindingResponseModel requestBindPage(@RequestBody AccountBindingRequestModel bindingRequest) {
AccountBindingResponseModel bindingResponse = new AccountBindingResponseModel();
try {
if (bindingRequest.getHeader().getNamespace().equals(BindingMode.AUTHORIZATION.getMode())) {
return mAccountBindingClient.doAccountBinding(bindingRequest);
} else {
return mAccountBindingClient.doAccountUnBinding(bindingRequest);
}
} catch (Exception ex) {
bindingResponse.setErrorMessage(ex.toString());
return bindingResponse;
}
}
/api/v1/users/bind is the address to which the binding or unbinding request from a Huawei ability gallery is sent. This address needs to be configured on the Fulfillment page in HUAWEI Ability Gallery. The request body of this service is in the following format.
Code:
{
"version":"1.0",
"header":{
"type":"Directive",
"timestamp":"15869741258",
"name":"AcceptGrant",
"namespace":"Authorization"
},
"inquire":{
"inquireId":"efd1f6ca-8fdf-11e8-9eb6-529269fb1459",
"payload":{
"grant":{
"type":"OAuth2.Authorization",
"openId":"ZU5uc3v045B6gkNSIXmf0414BQJQ5O37",
"sign":"TZ5uc3v0BQJQ5SIXmf0414O3745B6gkNXYZQPR",
"abilityId":"77fd5f3fc18243b58f2fe3e843991100"
}
}
}
}
OpenId: It is generated by Huawei Ability Gallery, using an app ID and a Huawei user ID.
Note: OpenId is unique for the same app and user.
3. Returns the Deep Link Of The Login Page
Deeplink needs to be created client-side has a specific syntax. This syntax should end with OpenID.
native app deep link example : app://com.huawei.hag/openId=xxxx
Click to expand...
Click to collapse
Code:
@Override
public AccountBindingResponseModel doAccountBinding(AccountBindingRequestModel bindingRequest) {
logger.debug("starts doAccountBinding");
AccountBindingResponseModel bindingResponse = new AccountBindingResponseModel();
bindingResponse.setErrorCode(SUCCESS);
bindingResponse.setErrorMessage("OK");
String openId = bindingRequest.getInquire().getPayload().getGrant().getOpenId();
try {
ValidatorUtils.checkArgument(!StringUtils.isEmpty(openId));
} catch (IllegalArgumentException e) {
bindingResponse.setErrorCode(INVALID_PARAMETER);
bindingResponse.setErrorMessage("param openid is invalid");
}
AccountDirectiveInteraction interaction = new AccountDirectiveInteraction();
NativeAppLinkInteraction deepLink = new NativeAppLinkInteraction();
String appName = ResourceBundle.getBundle("url").getString("appname");
String packagename = ResourceBundle.getBundle("url").getString("packagename");
deepLink.setAppName(appName);
deepLink.setAppPackage(packagename);
deepLink.setMinVersion(1L);
deepLink.setUrl("app://open.sigIn.page/openId=" + openId);
interaction.setDeepLink(deepLink);
Reply reply = new Reply();
reply.setAccountLoginAddr(interaction);
bindingResponse.setReply(reply);
bindingResponse.setVersion("1.0");
logger.debug("doAccountBinding ended successfully");
return bindingResponse;
}
5. Authentication / Login with developer’s server
A deep link is created in the desired format. Afterward, the user redirects to the app sign-in page via the mentioned link.
The user enters the authorization credentials on the sign-in page. The sign-in page sends the credentials and OpenID to the developer’s server. OpenId is obtained from account binding response.
Once the developer’s server has verified the credentials, it will bind the developer’s account to the user’s OpenID.
Code:
if(loginRequestModel.getOpenId() != null) {
mAccountBindingClient.sendBindRequest(loginRequestModel.getOpenId(), false);
foundUser.setOpenId(loginRequestModel.getOpenId());
userRepository.saveAndFlush(foundUser);
}
6. Binding Operation
We need to “accessToken” for binding operation. We can obtain “accessToken” while creating an HTTP post request
huawei.hag.bindUrl=
https://hag-eu.cloud.huawei.com/open-ability/v1/open-account-events/bind
Click to expand...
Click to collapse
Code:
@Override
public BindResponse sendBindRequest(String openId, Boolean unBind) throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
logger.debug("starts sendBindRequest with openId is {}", openId);
String serverUrl = "";
try {
OpenAccountBindReq bindReq = new OpenAccountBindReq();
bindReq.setOpenId(openId);
bindReq.setRequestTime(TimeUtil.toTimeString(new Date()));
StringEntity entity = new StringEntity(JSON.toJSONString(bindReq), "UTF-8");
if(unBind) {
serverUrl = ResourceBundle.getBundle("url").getString("huawei.hag.unBindUrl");
} else {
serverUrl = ResourceBundle.getBundle("url").getString("huawei.hag.bindUrl");
}
HttpResponse response = httpClient.execute(getHttpPost(serverUrl, entity));
String reponseContent = EntityUtils.toString(response.getEntity());
Integer status = response.getStatusLine().getStatusCode();
BindResponse bindResponse = new BindResponse();
JSONObject jsonObject = JSONObject.parseObject(reponseContent);
String code = jsonObject.getString("code");
String desc = jsonObject.getString("desc");
if (status == 200) {
bindResponse.setCode(code);
bindResponse.setDesc(desc);
logger.debug("sendBindRequest process ended successfully");
return bindResponse;
}
logger.debug("sendBindRequest process error, error code :{}, error desc : {}", code, desc);
throw new Exception("Send Request to " + serverUrl + " status is " + status);
} catch (Exception e) {
throw new Exception(e.toString());
} finally {
httpClient.close();
}
}
Code:
private HttpPost getHttpPost(String postUrl, StringEntity entity) throws Exception {
HttpPost httpPost = new HttpPost(postUrl);
try {
executeAccessToken();
httpPost.setHeader("Authorization", "Bearer " + accessToken);
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Accept", "application/json");
httpPost.setHeader("x-appid", appId);
httpPost.setEntity(entity);
return httpPost;
} catch (IOException ex) {
throw new IOException(ex.toString());
}
}
Code:
private void executeAccessToken() throws Exception {
String requestBody = createRequestBody(appId, appSecret);
HttpPost httpPost = new HttpPost(TOKEN_AT_URL);
CloseableHttpClient httpClient = HttpClients.createDefault();
try {
StringEntity entity = new StringEntity(requestBody);
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");
httpPost.setEntity(entity);
HttpResponse response = httpClient.execute(httpPost);
String jsonStr = EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
throw new Exception("Send Request to " + TOKEN_AT_URL + " failed with httpcode is " + statusCode);
}
JSONObject jsonObject = JSONObject.parseObject(jsonStr);
accessToken = jsonObject.getString("access_token");
} catch (IOException ex) {
throw new IOException(ex.toString());
} finally {
httpClient.close();
}
}
Account Unbinding
If the user taps the account unbinding button, Huawei Ability Gallery sends the request to the developer server (/api/v1/users/bind).
The request body of unbinding operation is the same as the binding operation’s body. There is only one difference and that is the namespace.
The namespace must be “Deauthorization” for unbinding operations.
Code:
{
"version":"1.0",
"header":{
"type":"Directive",
"timestamp":"15869741258",
"name":"AcceptGrant",
"namespace":"Deauthorization"
},
"inquire":{
"inquireId":"efd1f6ca-8fdf-11e8-9eb6-529269fb1459",
"payload":{
"grant":{
"type":"OAuth2.Authorization",
"openId":"ZU5uc3v045B6gkNSIXmf0414BQJQ5O37",
"sign":"TZ5uc3v0BQJQ5SIXmf0414O3745B6gkNXYZQPR",
"abilityId":"7a0af511a91f4591b4efbbaacd8bee60"
}
}
}
}
huawei.hag.unBindUrl=
https://hag-eu.cloud.huawei.com/open-ability/v1/open-account-events/unbind
Click to expand...
Click to collapse
Code:
@Override
public AccountBindingResponseModel doAccountUnBinding(AccountBindingRequestModel bindingRequest) throws Exception {
logger.debug("starts doAccountUnBinding");
try {
String openId = bindingRequest.getInquire().getPayload().getGrant().getOpenId();
BindResponse response = sendBindRequest(openId, true);
AccountBindingResponseModel bindingResponse = new AccountBindingResponseModel();
bindingResponse.setErrorCode(response.getCode());
bindingResponse.setErrorMessage(response.getDesc());
User foundUser = userRepository.findByOpenId(openId).orElse(null);
if (foundUser == null) {
logger.debug("user not found by open id, openId is {}", openId);
} else {
foundUser.setOpenId(null);
userRepository.saveAndFlush(foundUser);
}
logger.debug("doAccountUnBinding process ended successfully");
return bindingResponse;
} catch (Exception e) {
throw new Exception(e.toString());
}
}
Conclusion
I hope this article helps you better understand Huawei Ability and account binding/unbinding operations.
Thanks for reading. If you have a question, don’t hesitate to contact me.
References:
Document
developer.huawei.com
Document
developer.huawei.com
Document
developer.huawei.com
Provided clear info. thank you
yagmur.kilic said:
Introduction
This article. we will learn how to implement the Card Ability — Huawei Account binding — solution 1 step by step.
Account Binding
Account binding refers to the process of binding a user’s HUAWEI ID and a developer’s account when developing card ability. Huawei provides three solutions to developers for account binding. We will work on solution-1 in this article.
if everybody is ready, let’s get started
Implementation
The account binding process consists of six phases in total. The picture below will help you better understand the process of account binding.
View attachment 5340343
1.Enables Custom Sign Page — Account Binding Solution-1
Firstly, the developer server is mandatory for using solution-1. Huawei Ability Gallery sends the bind request to the developer’s server when custom sign mode is enabled from the Huawei phone.
2. Sends the Request to Obtain a Deep Link URL
Code:
@PostMapping("/api/v1/users/bind")
public AccountBindingResponseModel requestBindPage(@RequestBody AccountBindingRequestModel bindingRequest) {
AccountBindingResponseModel bindingResponse = new AccountBindingResponseModel();
try {
if (bindingRequest.getHeader().getNamespace().equals(BindingMode.AUTHORIZATION.getMode())) {
return mAccountBindingClient.doAccountBinding(bindingRequest);
} else {
return mAccountBindingClient.doAccountUnBinding(bindingRequest);
}
} catch (Exception ex) {
bindingResponse.setErrorMessage(ex.toString());
return bindingResponse;
}
}
/api/v1/users/bind is the address to which the binding or unbinding request from a Huawei ability gallery is sent. This address needs to be configured on the Fulfillment page in HUAWEI Ability Gallery. The request body of this service is in the following format.
Code:
{
"version":"1.0",
"header":{
"type":"Directive",
"timestamp":"15869741258",
"name":"AcceptGrant",
"namespace":"Authorization"
},
"inquire":{
"inquireId":"efd1f6ca-8fdf-11e8-9eb6-529269fb1459",
"payload":{
"grant":{
"type":"OAuth2.Authorization",
"openId":"ZU5uc3v045B6gkNSIXmf0414BQJQ5O37",
"sign":"TZ5uc3v0BQJQ5SIXmf0414O3745B6gkNXYZQPR",
"abilityId":"77fd5f3fc18243b58f2fe3e843991100"
}
}
}
}
OpenId: It is generated by Huawei Ability Gallery, using an app ID and a Huawei user ID.
Note: OpenId is unique for the same app and user.
3. Returns the Deep Link Of The Login Page
Deeplink needs to be created client-side has a specific syntax. This syntax should end with OpenID.
Code:
@Override
public AccountBindingResponseModel doAccountBinding(AccountBindingRequestModel bindingRequest) {
logger.debug("starts doAccountBinding");
AccountBindingResponseModel bindingResponse = new AccountBindingResponseModel();
bindingResponse.setErrorCode(SUCCESS);
bindingResponse.setErrorMessage("OK");
String openId = bindingRequest.getInquire().getPayload().getGrant().getOpenId();
try {
ValidatorUtils.checkArgument(!StringUtils.isEmpty(openId));
} catch (IllegalArgumentException e) {
bindingResponse.setErrorCode(INVALID_PARAMETER);
bindingResponse.setErrorMessage("param openid is invalid");
}
AccountDirectiveInteraction interaction = new AccountDirectiveInteraction();
NativeAppLinkInteraction deepLink = new NativeAppLinkInteraction();
String appName = ResourceBundle.getBundle("url").getString("appname");
String packagename = ResourceBundle.getBundle("url").getString("packagename");
deepLink.setAppName(appName);
deepLink.setAppPackage(packagename);
deepLink.setMinVersion(1L);
deepLink.setUrl("app://open.sigIn.page/openId=" + openId);
interaction.setDeepLink(deepLink);
Reply reply = new Reply();
reply.setAccountLoginAddr(interaction);
bindingResponse.setReply(reply);
bindingResponse.setVersion("1.0");
logger.debug("doAccountBinding ended successfully");
return bindingResponse;
}
5. Authentication / Login with developer’s server
A deep link is created in the desired format. Afterward, the user redirects to the app sign-in page via the mentioned link.
The user enters the authorization credentials on the sign-in page. The sign-in page sends the credentials and OpenID to the developer’s server. OpenId is obtained from account binding response.
Once the developer’s server has verified the credentials, it will bind the developer’s account to the user’s OpenID.
Code:
if(loginRequestModel.getOpenId() != null) {
mAccountBindingClient.sendBindRequest(loginRequestModel.getOpenId(), false);
foundUser.setOpenId(loginRequestModel.getOpenId());
userRepository.saveAndFlush(foundUser);
}
6. Binding Operation
We need to “accessToken” for binding operation. We can obtain “accessToken” while creating an HTTP post request
Code:
@Override
public BindResponse sendBindRequest(String openId, Boolean unBind) throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
logger.debug("starts sendBindRequest with openId is {}", openId);
String serverUrl = "";
try {
OpenAccountBindReq bindReq = new OpenAccountBindReq();
bindReq.setOpenId(openId);
bindReq.setRequestTime(TimeUtil.toTimeString(new Date()));
StringEntity entity = new StringEntity(JSON.toJSONString(bindReq), "UTF-8");
if(unBind) {
serverUrl = ResourceBundle.getBundle("url").getString("huawei.hag.unBindUrl");
} else {
serverUrl = ResourceBundle.getBundle("url").getString("huawei.hag.bindUrl");
}
HttpResponse response = httpClient.execute(getHttpPost(serverUrl, entity));
String reponseContent = EntityUtils.toString(response.getEntity());
Integer status = response.getStatusLine().getStatusCode();
BindResponse bindResponse = new BindResponse();
JSONObject jsonObject = JSONObject.parseObject(reponseContent);
String code = jsonObject.getString("code");
String desc = jsonObject.getString("desc");
if (status == 200) {
bindResponse.setCode(code);
bindResponse.setDesc(desc);
logger.debug("sendBindRequest process ended successfully");
return bindResponse;
}
logger.debug("sendBindRequest process error, error code :{}, error desc : {}", code, desc);
throw new Exception("Send Request to " + serverUrl + " status is " + status);
} catch (Exception e) {
throw new Exception(e.toString());
} finally {
httpClient.close();
}
}
Code:
private HttpPost getHttpPost(String postUrl, StringEntity entity) throws Exception {
HttpPost httpPost = new HttpPost(postUrl);
try {
executeAccessToken();
httpPost.setHeader("Authorization", "Bearer " + accessToken);
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Accept", "application/json");
httpPost.setHeader("x-appid", appId);
httpPost.setEntity(entity);
return httpPost;
} catch (IOException ex) {
throw new IOException(ex.toString());
}
}
Code:
private void executeAccessToken() throws Exception {
String requestBody = createRequestBody(appId, appSecret);
HttpPost httpPost = new HttpPost(TOKEN_AT_URL);
CloseableHttpClient httpClient = HttpClients.createDefault();
try {
StringEntity entity = new StringEntity(requestBody);
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");
httpPost.setEntity(entity);
HttpResponse response = httpClient.execute(httpPost);
String jsonStr = EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
throw new Exception("Send Request to " + TOKEN_AT_URL + " failed with httpcode is " + statusCode);
}
JSONObject jsonObject = JSONObject.parseObject(jsonStr);
accessToken = jsonObject.getString("access_token");
} catch (IOException ex) {
throw new IOException(ex.toString());
} finally {
httpClient.close();
}
}
Account Unbinding
If the user taps the account unbinding button, Huawei Ability Gallery sends the request to the developer server (/api/v1/users/bind).
The request body of unbinding operation is the same as the binding operation’s body. There is only one difference and that is the namespace.
The namespace must be “Deauthorization” for unbinding operations.
Code:
{
"version":"1.0",
"header":{
"type":"Directive",
"timestamp":"15869741258",
"name":"AcceptGrant",
"namespace":"Deauthorization"
},
"inquire":{
"inquireId":"efd1f6ca-8fdf-11e8-9eb6-529269fb1459",
"payload":{
"grant":{
"type":"OAuth2.Authorization",
"openId":"ZU5uc3v045B6gkNSIXmf0414BQJQ5O37",
"sign":"TZ5uc3v0BQJQ5SIXmf0414O3745B6gkNXYZQPR",
"abilityId":"7a0af511a91f4591b4efbbaacd8bee60"
}
}
}
}
Code:
@Override
public AccountBindingResponseModel doAccountUnBinding(AccountBindingRequestModel bindingRequest) throws Exception {
logger.debug("starts doAccountUnBinding");
try {
String openId = bindingRequest.getInquire().getPayload().getGrant().getOpenId();
BindResponse response = sendBindRequest(openId, true);
AccountBindingResponseModel bindingResponse = new AccountBindingResponseModel();
bindingResponse.setErrorCode(response.getCode());
bindingResponse.setErrorMessage(response.getDesc());
User foundUser = userRepository.findByOpenId(openId).orElse(null);
if (foundUser == null) {
logger.debug("user not found by open id, openId is {}", openId);
} else {
foundUser.setOpenId(null);
userRepository.saveAndFlush(foundUser);
}
logger.debug("doAccountUnBinding process ended successfully");
return bindingResponse;
} catch (Exception e) {
throw new Exception(e.toString());
}
}
Conclusion
I hope this article helps you better understand Huawei Ability and account binding/unbinding operations.
Thanks for reading. If you have a question, don’t hesitate to contact me.
References:
Document
developer.huawei.com
Document
developer.huawei.com
Document
developer.huawei.com
Click to expand...
Click to collapse
Its informative and clear
{
"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 that chat option between two people, they can share text between each other. The application needs to have instant messaging so once a user sends the message to a friend over the application, the friend will receive the push notification at the given time. The quintessence of an app like instant application is available and react to ongoing actions. Also, push notifications can be an excellent promotional tool that can be used to inform users about updates and new functionalities.
Huawei Kits Used
Huawei Cloud DB
Huawei Auth Service
Huawei Cloud function.
Huawei Push Kit
Huawei API Used
Huawei CloudDB API - Cloud DB is used to store users data, users chat and also used to manage users chat history with other users.
a) Upsert
i) Insert data of the users from the profile.
ii) Create and insert room id, room id is consider as a reference between two users chat. Using room id will store all the respective chat data in the DB.
iii) Insert Chat data between two users based on the room id.
b) Query
i) Get list of Contacts for chat.
ii) Get list of users with whom logged in user chatted before.
ii) Get details of the chat screen with all the chat messages which includes images, text and location.
Huawei Auth Service – Using the Auth Service we are registering the user on the Ecosystem. We are using the Phone number auth service for the same to receive the OTP and verify the user here.
Huawei Cloud function – We are triggering the Huawei Push notification system using cloud function for the same.
Huawei Push kit - Push kit is used to push notification of message to other user. So when one user send message it will notify other user through push notification only.
Used the rest end point for the cloud function to send the push notification once the message is end, trigger from the device.
On HMSMessage Received This is once parsing the data as per our need on the implementation, so we need to parse image and location when shared by other success.
Database structure
Now it's time to create project on Huawei console and developmentIntegration PreparationsYou must complete the following preparations:
Register as a developer on Huawei console.
Create a project and an app in AppGallery Connect.
Generate and configure the signing certificate fingerprint.
Enable Auth service, Push and Cloud DB.
For details, refer to Configuring App Information in AppGallery Connect for HMS
First create cloud DB Zones
Create 3 object types
ChatRoomId: contain all chatting room id.
User: all register user details.
UserChat: all users chat details.
Let's start development with Login Page
We will login with Phone number using HMS Auth service.
Enable Phone number Authentication mode as shown in below image.
Add dependency
XML:
// HMS dependencies
implementation "com.huawei.agconnect:agconnect-database:$rootProject.ext.agdatabase"
implementation "com.huawei.agconnect:agconnect-auth:$rootProject.ext.agauth"
implementation "com.huawei.hms:push:$rootProject.ext.pushkit"
Enter the valid phone number, we will get OTP on same number using below method.
GET OTP
Java:
VerifyCodeSettings settings = new VerifyCodeSettings.Builder()
.action(VerifyCodeSettings.ACTION_REGISTER_LOGIN)
.sendInterval(30)
.locale(Locale.getDefault())
.build();
Task<VerifyCodeResult> task = AGConnectAuth.getInstance().requestVerifyCode(countryCodeStr, phoneNumberStr, settings);
task.addOnSuccessListener(TaskExecutors.immediate(), verifyCodeResult -> {
if (null != verifyCodeResult) {
verifyCodeResultMutableLiveData.postValue(verifyCodeResult);
}
});
task.addOnFailureListener(e ->
AppLog.logE(TAG, "onFailure: " + e.getCause()));
Verify Contact details
Java:
PhoneUser phoneUser = new PhoneUser.Builder()
.setCountryCode(countryCodeStr)
.setPhoneNumber(phoneNumberStr)
.setVerifyCode(code)
.build();
AGConnectAuth.getInstance().createUser(phoneUser)
.addOnSuccessListener(signInResult -> {
if (signInResult != null) {
User user = new User();
user.setUsername(signInResult.getUser().getDisplayName());
user.setPhoneNumber(phoneNumberStr);
userMutableLiveData.postValue(user);
}
})
.addOnFailureListener(e -> {
Log.e(TAG, "verifyContactDetails: " + e.getStackTrace());
User user = new User();
user.setPhoneNumber(phoneNumberStr);
userMutableLiveData.setValue(user);
});
After verify, user can authenticate successfully.
Now we have to complete the user profile, as in below picture you can see we have only phone number.
Create/ Update profile
Java:
public void saveUser(User user, Context context) {
CloudDBHelper.getInstance().openDb((isConnected, cloudDBZone) -> {
if (isConnected && cloudDBZone != null) {
if (cloudDBZone == null) {
return;
} else {
Task<Integer> insertTask = cloudDBZone.executeUpsert(user);
insertTask.addOnSuccessListener(integer -> {
userMutableLiveData.setValue(true);
CloudDBHelper.getInstance().closeDb(context);
}).addOnFailureListener(e -> {
userMutableLiveData.setValue(false);
CloudDBHelper.getInstance().closeDb(context);
});
}
}
});
}
Generate push token
Java:
GetToken getToken = new GetToken(app_id, UserProfileActivity.this);
getToken.setGetTokenListener(this);
getToken.execute();
GetToken class is a service class to generate push token
Java:
public class GetToken extends AsyncTask<Void, Void, String> {
private static final String TAG = GetToken.class.getSimpleName();
private Context context;
private String appId;
private GetTokenListener getTokenListener;
public GetToken(String appId, Context context) {
this.appId = appId;
this.context = context;
}
public void setGetTokenListener(GetTokenListener getTokenListener) {
this.getTokenListener = getTokenListener;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected String doInBackground(Void... voids) {
try {
String pushToken = HmsInstanceId.getInstance(context).getToken(appId, HmsMessaging.DEFAULT_TOKEN_SCOPE);
AppLog.logD(TAG, pushToken);
getTokenListener.getToken(pushToken);
return pushToken;
} catch (ApiException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
}
}
It will create the user profile and update data into Cloud DB.
Start chat for first time
We have to create room id for chatting between two people.
Java:
public void callCreateRoomId(final String userMobileTo, final String userMobileFrom) {
CloudDBHelper.getInstance().openDb(new OnDBZoneOpen() {
@Override
public void isDBZoneOpen(boolean isConnected, CloudDBZone cloudDBZone) {
if (cloudDBZone != null) {
ChatRoomId roomId = new ChatRoomId();
mRoomDataIndex = mRoomDataIndex + 1;
AppLog.logE(TAG, "New ROOM IS WILL BE ===> " + mRoomDataIndex);
roomId.setRoom_id(String.valueOf(mRoomDataIndex));
roomId.setUser_mobile_to(userMobileTo);
roomId.setUser_mobile_from(userMobileFrom);
roomId.setUpdate_shadow_flag(true);
Task<Integer> insertTask = cloudDBZone.executeUpsert(roomId);
insertTask.addOnSuccessListener(insertSuccessListener(roomId))
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
AppLog.logE(TAG, "Exception in creating room id " + e.getLocalizedMessage());
}
}).addOnCanceledListener(new OnCanceledListener() {
@Override
public void onCanceled() {
AppLog.logE(TAG, "Inside on cancel listener");
}
});
} else {
if (mOnApiError != null) {
mOnApiError.onError("Cloud database zone is null", new Throwable("CloudDBZone is null"));
}
}
}
});
}
Get previous chats from cloudDB
Java:
public void getUserChatByRoomID(String roomId, Context context) {
CloudDBZoneQuery<UserChat> query = CloudDBZoneQuery.where(UserChat.class).equalTo(DBConstants.roomId, roomId).orderByAsc(DBConstants.MESSAGE_TIMESTAMP);
getUserChat(query, context);
}
private void getUserChat(CloudDBZoneQuery<UserChat> userQuery, Context context) {
CloudDBHelper.getInstance().openDb((isConnected, cloudDBZone) -> {
Task<CloudDBZoneSnapshot<UserChat>> queryTask = cloudDBZone.executeQuery(userQuery,
CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY);
queryTask.addOnSuccessListener(userChatCloudDBZoneSnapshot -> {
processSnapShot(userChatCloudDBZoneSnapshot.getSnapshotObjects(), context);
});
});
}
private void processSnapShot(CloudDBZoneObjectList<UserChat> userCloudDBZoneSnapshot, Context context) {
if (userCloudDBZoneSnapshot != null) {
ArrayList<UserChat> users = new ArrayList<>();
while (userCloudDBZoneSnapshot.hasNext()) {
UserChat user = null;
try {
user = userCloudDBZoneSnapshot.next();
users.add(user);
} catch (AGConnectCloudDBException e) {
e.printStackTrace();
CloudDBHelper.getInstance().closeDb(context);
}
}
userChatMutableLiveData.setValue(users);
CloudDBHelper.getInstance().closeDb(context);
}
}
Start Sending chat messages
messageType: user can send message in text, image, location or in video Types.
Java:
private void setMessage(String messageType) {
Util.showProgressBar(MessageActivity.this);
UserChat userChat = new UserChat();
userChat.setRoom_id(roomId);
userChat.setMessage_timestamp(Long.parseLong(Util.getTimeStamp()));
userChat.setChat_id(Util.getRandomNumber());
userChat.setReceiver_name(receiverText);
userChat.setReceiver_phone(receiverPhoneNumber);
userChat.setSender_name(ChitChatSharedPref.getInstance().getString(Constants.USER_NAME, ""));
userChat.setSender_phone(ChitChatSharedPref.getInstance().getString(Constants.PHONE_NUMBER, ""));
userChat.setMessage_type(messageType);
switch (messageType) {
case Constants.MESSAGE_TYPE_TEXT:
userChat.setMessage_data(textSend.getText().toString());
messageViewModel.saveUserChat(userChat);
messageViewModel.userUpdatedSuccessfully.observe(MessageActivity.this, aBoolean -> {
if (aBoolean) {
Util.stopProgressBar();
getChatList();
} else {
Util.stopProgressBar();
}
});
break;
}
messageViewModel.queryForToken(receiverPhoneNumber, MessageActivity.this);
}
It will save user data into cloud dB
Java:
public void saveUserChat(UserChat userChat) {
CloudDBHelper.getInstance().openDb((isConnected, cloudDBZone) -> {
if (cloudDBZone != null) {
Task<Integer> insertTask = cloudDBZone.executeUpsert(userChat);
insertTask
.addOnSuccessListener(integer ->
userUpdatedSuccessfully.setValue(true))
.addOnFailureListener(e -> {
userUpdatedSuccessfully.setValue(false);
AppLog.logE(TAG, e.getMessage());
});
} else {
userUpdatedSuccessfully.setValue(false);
}
});
}
It's time to send push notification
Java:
public void queryForToken(String phoneNumber, Context context) {
CloudDBZoneQuery<User> query = CloudDBZoneQuery.where(User.class).equalTo(DBConstants.userNumber, phoneNumber);
processNumberCheck(query, context);
}
messageViewModel.tokenMutableLiveData.observe(MessageActivity.this, s -> {
PushApis pushApis = new PushApis(MessageActivity.this);
if (messageType.equalsIgnoreCase(Constants.MESSAGE_TYPE_TEXT)) {
pushApis.sendPushNotification(roomId, messageType, "104739093", MessageActivity.this.textSend.getText().toString(), s);
textSend.setText("");
}
});
Setting up push messaging API's
Java:
public class PushApis {
private Context context;
public PushApis(Context context) {
this.context = context;
}
public void sendPushNotification(String chatId, String message, String appId, String messageData, String userPushTokens) {
try {
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
String response = "";
URL url = new URL(Constants.TOKEN_URL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("POST", "/oauth2/v3/token HTTP/1.1");
connection.setRequestProperty("Host", "oauth-login.cloud.huawei.com");
HashMap<String, String> params = new HashMap<>();
params.put("grant_type", "client_credentials");
params.put("client_secret", Constants.CLIENT_SECRET);
params.put("client_id", Constants.CLIENT_ID);
String postDataLength = getDataString(params);
OutputStream os = connection.getOutputStream();
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(os, "UTF-8"));
writer.write(postDataLength);
writer.flush();
writer.close();
os.close();
int responseCode = connection.getResponseCode();
if (responseCode == HttpsURLConnection.HTTP_OK) {
String line;
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
while ((line = br.readLine()) != null) {
response += line;
}
} else {
response = "";
}
AppLog.logE("Response", response);
Gson gson = new Gson();
BearerRequest bearerRequest = gson.fromJson(response, BearerRequest.class);
triggerPush(bearerRequest.getAccess_token(), appId, chatId, message, messageData, userPushTokens);
} catch (Exception e) {
e.printStackTrace();
}
}
private String getDataString(HashMap<String, String> params) throws UnsupportedEncodingException {
StringBuilder result = new StringBuilder();
boolean first = true;
for (Map.Entry<String, String> entry : params.entrySet()) {
if (first)
first = false;
else
result.append("&");
result.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
result.append("=");
result.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
}
return result.toString();
}
private void triggerPush(String bearer, String appId, String chatId, String messageType, String messageData, String userPushTokens) {
try {
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
String response = null;
URL url = new URL("https://push-api.cloud.huawei.com/v1/" + appId + "/messages:send");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("Authorization", "Bearer " + bearer);
connection.setRequestProperty("Host", "oauth-login.cloud.huawei.com");
connection.setRequestProperty("POST", "/oauth2/v2/token HTTP/1.1");
OutputStream os = connection.getOutputStream();
Data data = new Data();
data.message = messageType;
data.roomId = chatId;
data.messageData = messageData;
data.sender_name = senderName;
data.sender_phone = senderPhone;
data.title = context.getResources().getString(R.string.app_name);
ArrayList<String> token = new ArrayList<>();
token.add(userPushTokens);
Message message = new Message();
message.tokens = token;
message.data = data.toString();
PushMessageRequest pushMessageRequest = new PushMessageRequest();
pushMessageRequest.message = message;
pushMessageRequest.validate_only = false;
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(os, "UTF-8"));
Gson gson = new Gson();
JSONObject jsonObject = new JSONObject(gson.toJson(pushMessageRequest, PushMessageRequest.class));
writer.write(jsonObject.toString());
writer.flush();
writer.close();
os.close();
int responseCode = connection.getResponseCode();
String line = null;
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
while ((line = br.readLine()) != null) {
response += line;
}
AppLog.logE("Response", response);
} catch (Exception e) {
e.getStackTrace();
}
}
}
Conclusion
In this article, we have learned how we can create a simple messaging application with cloud dB, auth service and push kit. We can also use cloud storage for store profile picture, location, documents or audio and video files.
Thanks for reading this article. Be sure to like and comment to this article, if you found it helpful. It means a lot to me.
Reference
https://developer.huawei.com/consumer/en/agconnect/cloud-base/
https://developer.huawei.com/consum...-Guides/service-introduction-0000001050040060
https://developer.huawei.com/consumer/en/agconnect/auth-service/