Related
Following the Google documentation I create an app that insert new events to the user's calendar.
In the following code you can see how I did it.
MakeRequastTask.class
Code:
/**
* An asynchronous task that handles the Google Calendar API call.
* Placing the API calls in their own task ensures the UI stays responsive.
*/
private class MakeRequestTask extends AsyncTask<Void, Void, String[][]> {
private com.google.api.services.calendar.Calendar mServiceCalendar = null;
private Exception mLastError = null;
private String[][] tempArray;
private SpreadSheet mSheet;
MakeRequestTask(GoogleAccountCredential credential, SpreadSheet mSheet) {
this.mSheet = mSheet;
tempArray = new String[7][2];
HttpTransport transport = AndroidHttp.newCompatibleTransport();
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
mServiceCalendar = new com.google.api.services.calendar.Calendar.Builder(
transport, jsonFactory, credential)
.setApplicationName("Bank Of Israel")
.build();
}
/**
* Background task to call Google Calendar API.
* @param params no parameters needed for this task.
*/
@Override
protected String[][] doInBackground(Void... params) {
try {
setDataInCalendar();
} catch (Exception e) {
mLastError = e;
cancel(true);
return null;
}
return null;
}
//--- Google Calendar ---\\
private void setDataInCalendar() throws IOException {
Vector<Shift> temp = mSheet.getShifts();
for(Shift s: temp) {
Event e = createEvent(s);
String calendarId = userEmailAddress;
e = mServiceCalendar.events().insert(calendarId, e).execute();
Log.d("MyApp", "Event created: " + e.getHtmlLink());
}
}
private Event createEvent(Shift mShift) {
final String TimeOffSet = "-07:00";
Event event = new Event();
event.setSummary(mShift.getShiftName());//type
//set Date, Start & End Times
SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd");
//make a String for the DateTime Object
//format as: "yyyy-MM-ddTHH:mm:ss+UTCOffSet"
String startTimeUTC = sd.format(mShift.getShiftDate()) +"T"+
mShift.getStartTime().toString() + TimeOffSet ;
DateTime startDateTime = new DateTime(startTimeUTC);
EventDateTime start = new EventDateTime()
.setDateTime(startDateTime);
event.setStart(start);
String endTimeUTC;
//if the shift is C or C1 the end of the shift is in the next day
if (mShift.getShiftName().equals("c1")
||
mShift.getShiftName().equals("c")){
endTimeUTC = sd.format(PatternTest.oneDayForeword(mShift.getShiftDate())) +"T"+
mShift.getEndTime().toString()+ TimeOffSet;
}
else{
endTimeUTC = sd.format(mShift.getShiftDate()) +"T"+
mShift.getEndTime().toString() + IsraelTimeOffSet;
}
DateTime endDateTime = new DateTime(endTimeUTC);
EventDateTime end = new EventDateTime()
.setDateTime(endDateTime);
event.setEnd(end);
event.setColorId("10");
return event;
}
//------------------------------------------------------------------------------------\\
@Override
protected void onPreExecute() {
super.onPreExecute();
showProgressDialog();
}
@Override
protected void onPostExecute(String[][] strings) {
super.onPostExecute(strings);
hideProgressDialog();
Toast.makeText(SheetActivity.this, "Finish!", Toast.LENGTH_LONG).show();
}
}
@Override
protected void onCancelled() {
hideProgressDialog();
if (mLastError != null) {
if (mLastError instanceof GooglePlayServicesAvailabilityIOException) {
showGooglePlayServicesAvailabilityErrorDialog(
((GooglePlayServicesAvailabilityIOException) mLastError)
.getConnectionStatusCode());
} else if (mLastError instanceof UserRecoverableAuthIOException) {
startActivityForResult(
((UserRecoverableAuthIOException) mLastError).getIntent(),
SheetActivity.REQUEST_AUTHORIZATION);
}
else if (mLastError instanceof NoShiftsInFileException){
Toast.makeText(SheetActivity.this, mLastError.getMessage() , Toast.LENGTH_SHORT).show();
}
else if (mLastError instanceof NoUserNameException){
Toast.makeText(SheetActivity.this, mLastError.getMessage() , Toast.LENGTH_LONG).show();
finish();//go back to the SignIn Activity
}
else {
Toast.makeText(SheetActivity.this, "The following error occurred:\n"
+ mLastError.getMessage(), Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(SheetActivity.this, "Request cancelled." , Toast.LENGTH_SHORT).show();
}
}
}
My problem is that after my app insert the new events, I can see them in the web Calendar but the Google Calendar App or any other calendar app I use on my smartphone (LG G2) doesn't show the new events (even after refreshing and resyncing), however when I'm inserting a new event using the web page the event sync perfectly with the apps on my phone.
Thank in advenc
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
I am trying to access the googleFit API.
It seems pretty straightforward. Get the google sign-in permissions and required authorizations then query for Step count.
My code doesn't seem to work.
and it logs only "Error!!" that's it.
Android gurus, Where am I going wrong??
Code:
fun getAuthorizationAndReadData() {
try {
MainActivity().fitSignIn(FitActionRequestCode.READ_DATA)
} catch () {
Log.i("e", "error!!!!")
}
}
MainActivity.kt
Code:
enum class FitActionRequestCode {
READ_DATA
}
private val fitnessOptions: GoogleSignInOptionsExtension = FitnessOptions.builder()
.addDataType(DataType.TYPE_STEP_COUNT_DELTA, FitnessOptions.ACCESS_READ).build()
fun fitSignIn(requestCode: FitActionRequestCode) {
if (oAuthPermissionsApproved()) {
readHistoryData()
} else {
requestCode.let {
GoogleSignIn.requestPermissions(
this,
requestCode.ordinal,
getGoogleAccount(), fitnessOptions)
}
}
}
private fun getGoogleAccount() = GoogleSignIn.getAccountForExtension(this, fitnessOptions)
private fun oAuthPermissionsApproved() = GoogleSignIn.hasPermissions(getGoogleAccount(), fitnessOptions)
private fun performActionForRequestCode(requestCode: FitActionRequestCode) = when (requestCode) {
FitActionRequestCode.READ_DATA -> readHistoryData()
}
private fun readHistoryData(): Task<DataReadResponse> {
// Begin by creating the query.
val readRequest = queryFitnessData()
// Invoke the History API to fetch the data with the query
return Fitness.getHistoryClient(this, getGoogleAccount())
.readData(readRequest)
.addOnSuccessListener { dataReadResponse ->
printData(dataReadResponse)
Log.i(ContentValues.TAG, "Data read was successful!") }
.addOnFailureListener { e ->
Log.e(ContentValues.TAG, "There was a problem reading the data.", e)
}
}
private fun queryFitnessData(): DataReadRequest {
// [START build_read_data_request]
// Setting a start and end date using a range of 1 week before this moment.
val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
val now = Date()
calendar.time = now
val endTime = calendar.timeInMillis
calendar.add(Calendar.WEEK_OF_YEAR, -1)
val startTime = calendar.timeInMillis
return DataReadRequest.Builder()
.aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA)
.bucketByTime(1, TimeUnit.DAYS)
.setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
.build()
}
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/