Related
Hi!
I have a problem... again
In my layout I have a container which I want to replace with
a) a Google map view
b) a text view
This must be configurable by a settings fragment. Everything works fine. But every time I switch from text to map or visa verse my app crashes with an exception.
java.lang.IllegalStateException: Fragment already active
I don't know what I am doing wrong. After switching from map to text within my settings I call this getMainActivity().setView() method:
Code:
protected void setView(boolean setMap) {
if (setMap == true) {
Log.i(TAG, "Inflating Google Maps fragment");
// Get a new Fragment Manager
fm = getFragmentManager();
FragmentTransaction fta = fm.beginTransaction();
// New instance if none exists
if (this.fragmentMap == null)
{
Log.d(TAG, "map fragment is null");
this.fragmentMap = new FragmentMap();
}
// Ok... if the fragment is not already added we will do it here right now
if (!this.fragmentMap.isAdded())
{
Log.d(TAG, "map fragment not added yet");
// Create a new bundle for the fragment
Bundle args = new Bundle();
this.fragmentMap.setArguments(args);
fta.replace(R.id.container, this.fragmentMap).commit();
} else
{
fta.show(this.fragmentMap).commit();
}
this.useMap = true;
} else if (setMap == false) {
Log.i(TAG, "Inflating text fragment");
// Get a new Fragment Manager
fm = getFragmentManager();
FragmentTransaction fta = fm.beginTransaction();
// New instance if none exists
if (this.fragmentText == null)
{
Log.d(TAG, "textfragment is null");
this.fragmentText = new FragmentText();
}
// Ok... if the fragment is not already added we will do it here right now
if (this.fragmentText != null)
{
// Create a new bundle for the fragment
Bundle args = new Bundle();
Log.d(TAG, "textfragment not yet added");
this.fragmentText.setArguments(args);
fta.replace(R.id.container, this.fragmentText).commit();
} else
{
fta.show(this.fragmentText).commit();
}
this.useMap = false;
}
}
The last messages I get before the exception is thrown are the "....fragment not yet added" messages.
How can I switch between a map fragment and a text fragment?
Thorsten
More information like this, you can visit HUAWEI Developer Forum
Original link: https://forums.developer.huawei.com/forumPortal/en/topicview?tid=0202330537081990041&fid=0101187876626530001
{
"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"
}
Article Introduction
In this article we are going to cover HUAWEI Map Kit JavaScript API introduction. Next we going to implementing HUAWEI Map in Ionic/Cordova project. Lastly we will implement HUAWEI Map Kit JavaScript into Native Application.
Technology Introduction
HUAWEI Map Kit provides JavaScript APIs for you to easily build map apps applicable to browsers.
It provides the basic map display, map interaction, route planning, place search, geocoding, and other functions to meet requirements of most developers.
Restriction
Before using the service, you need to apply for an API key on the HUAWEI Developers website. For details, please refer to "Creating an API Key" in API Console Operation Guide. To enhance the API key security, you are advised to restrict the API key. You can configure restrictions by app and API on the API key editing page.
Generating API Key
Go to HMS API Services > Credentials and click Create credential.
Click API key to generate new API Key.
In the dialog box that is displayed, click Restrict to set restrictions on the key to prevent unauthorized use or quota theft. This step is optional.
The restrictions include App restrictions and API restriction.
App restrictions: control which websites or apps can use your key. Set up to one app restriction per key.
API restrictions: specify the enabled APIs that this key can call.
After setup App restriction and API restriction API key will generate.
The API key is successfully created. Copy API Key to use in your project.
Huawei Web Map API introduction
1. Make a Basic Map
Code:
function loadMapScript() {
const apiKey = encodeURIComponent(
"API_KEY"
);
const src = `https://mapapi.cloud.huawei.com/mapjs/v1/api/js?callback=initMap&key=${apiKey}`;
const mapScript = document.createElement("script");
mapScript.setAttribute("src", src);
document.head.appendChild(mapScript);
}
function initMap() { }
function initMap() {
const mapOptions = {};
mapOptions.center = { lat: 48.856613, lng: 2.352222 };
mapOptions.zoom = 8;
mapOptions.language = "ENG";
const map = new HWMapJsSDK.HWMap(
document.getElementById("map"),
mapOptions
);
}
loadMapScript();
Note: Please update API_KEY with the key which you have generated. In script url we are declaring callback function, which will automatically initiate once Huawei Map Api loaded successfully.
2. Map Interactions
Map Controls
Code:
var mapOptions = {};
mapOptions.center = {lat: 48.856613, lng: 2.352222};
mapOptions.zoom = 10;
scaleControl
mapOptions.scaleControl = true; // Set to display the scale.
mapOptions.scaleControlOptions = {
units: "imperial" // Set the scale unit to inch.
};
zoomSlider
Code:
mapOptions.zoomSlider = true ; // Set to display the zoom slider.
zoomControl
Code:
mapOptions.zoomControl = false; // Set not to display the zoom button.
rotateControl (Manage Compass)
Code:
mapOptions.rotateControl = true; // Set to display the compass.
navigationControl
Code:
mapOptions.navigationControl = true; // Set to display the pan button.
copyrightControl
Code:
mapOptions.copyrightControl = true; // Set to display the copyright information.
mapOptions.copyrightControlOptions = {value: "HUAWEI",} // Set the copyright information.
locationControl
Code:
mapOptions.locationControl= true; // Set to display the current location.
Camera
Map moving: You can call the map.panTo(latLng)
Map shift: You can call the map.panBy(x, y)
Zoom: You can use the map.setZoom(zoom) method to set the zoom level of a map.
Area control: You can use map.fitBounds(bounds) to set the map display scope.
Map Events
Map click event:
Code:
map.on('click', () => {
map.zoomIn();
});
Map center change event:
Code:
map.onCenterChanged(centerChangePost);
function centerChangePost() {
var center = map.getCenter();
alert( 'Lng:'+map.getCenter().lng+'
'+'Lat:'+map.getCenter().lat);
}
Map heading change event:
Code:
map.onHeadingChanged(headingChangePost);
function headingChangePost() {
alert('Heading Changed!');
}
Map zoom level change event:
Code:
map.onZoomChanged(zoomChangePost);
function zoomChangePost() {
alert('Zoom Changed!')
}
3. Drawing on Map
Marker:
You can add markers to a map to identify locations such as stores and buildings, and provide additional information with information windows.
Code:
var map;
var mMarker;
function initMap() {
var mapOptions = {};
mapOptions.center = {lat: 48.856613, lng: 2.352222};
mapOptions.zoom = 8;
map = new HWMapJsSDK.HWMap(document.getElementById('map'), mapOptions);
mMarker = new HWMapJsSDK.HWMarker({
map: map,
position: {lat: 48.85, lng: 2.35},
zIndex: 10,
label: 'A',
icon: {
opacity: 0.5
}
});
}
Marker Result:
Marker Clustering:
The HMS Core Map SDK allows you to cluster markers to effectively manage them on the map at different zoom levels. When a user zooms in on the map to a high level, all markers are displayed on the map. When the user zooms out, the markers are clustered on the map for orderly display.
Code:
var map;
var markers = [];
var markerCluster;
var locations = [
{lat: 51.5145160, lng: -0.1270060},
{ lat : 51.5064490, lng : -0.1244260 },
{ lat : 51.5097080, lng : -0.1200450 },
{ lat : 51.5090680, lng : -0.1421420 },
{ lat : 51.4976080, lng : -0.1456320 },
···
{ lat : 51.5061590, lng : -0.140280 },
{ lat : 51.5047420, lng : -0.1470490 },
{ lat : 51.5126760, lng : -0.1189760 },
{ lat : 51.5108480, lng : -0.1208480 }
];
function initMap() {
var mapOptions = {};
mapOptions.center = {lat: 48.856613, lng: 2.352222};
mapOptions.zoom = 3;
map = new HWMapJsSDK.HWMap(document.getElementById('map'), mapOptions);
generateMarkers(locations);
markerCluster = new HWMapJsSDK.HWMarkerCluster(map, markers);
}
function generateMarkers(locations) {
for (let i = 0; i < locations.length; i++) {
var opts = {
position: locations[i]
};
markers.push(new HWMapJsSDK.HWMarker(opts));
}
}
Cluster markers Result:
Information Window:
The HMS Core Map SDK supports the display of information windows on the map. There are two types of information windows: One is to display text or image independently, and the other is to display text or image in a popup above a marker. The information window provides details about a marker.
Code:
var infoWindow;
function initMap() {
var mapOptions = {};
mapOptions.center = {lat: 48.856613, lng: 2.352222};
mapOptions.zoom = 8;
var map = new HWMapJsSDK.HWMap(document.getElementById('map'), mapOptions);
infoWindow = new HWMapJsSDK.HWInfoWindow({
map,
position: {lat: 48.856613, lng: 2.352222},
content: 'This is to show mouse event of another marker',
offset: [0, -40],
});
}
Info window Result:
Ground Overlay
The builder function of GroundOverlay uses the URL, LatLngBounds, and GroundOverlayOptions of an image as the parameters to display the image in a specified area on the map. The sample code is as follows:
Code:
var map;
var mGroundOverlay;
function initMap() {
var mapOptions = {};
mapOptions.center = {lat: 48.856613, lng: 2.352222};
mapOptions.zoom = 8;
map = new HWMapJsSDK.HWMap(document.getElementById('map'), mapOptions);
var imageBounds = {
north: 49,
south: 48.5,
east: 2.5,
west: 1.5,
};
mGroundOverlay = new HWMapJsSDK.HWGroundOverlay(
// Path to a local image or URL of an image.
'huawei_logo.png',
imageBounds,
{
map: map,
opacity: 1,
zIndex: 1
}
);
}
Marker Result:
Ionic / Cordova Map Implementation
In this part of article we are supposed to add Huawei Map Javascript API’s.
Update Index.html to implment Huawei Map JS scripts:
You need to update src/index.html and include Huawei map javacript cloud script url.
Code:
function loadMapScript() {
const apiKey = encodeURIComponent(
"API_KEY"
);
const src = `https://mapapi.cloud.huawei.com/mapjs/v1/api/js?callback=initMap&key=${apiKey}`;
const mapScript = document.createElement("script");
mapScript.setAttribute("src", src);
document.head.appendChild(mapScript);
}
function initMap() { }
loadMapScript();
Make new Map page:
Code:
ionic g page maps
Update maps.page.ts file and update typescript:
Code:
import { Component, OnInit, ChangeDetectorRef } from "@angular/core";
import { Observable } from "rxjs";
declare var HWMapJsSDK: any;
declare var cordova: any;
@Component({
selector: "app-maps",
templateUrl: "./maps.page.html",
styleUrls: ["./maps.page.scss"],
})
export class MapsPage implements OnInit {
map: any;
baseLat = 24.713552;
baseLng = 46.675297;
ngOnInit() {
this.showMap(his.baseLat, this.baseLng);
}
ionViewWillEnter() {
}
ionViewDidEnter() {
}
showMap(lat = this.baseLat, lng = this.baseLng) {
const mapOptions: any = {};
mapOptions.center = { lat: lat, lng: lng };
mapOptions.zoom = 10;
mapOptions.language = "ENG";
this.map = new HWMapJsSDK.HWMap(document.getElementById("map"), mapOptions);
this.map.setCenter({ lat: lat, lng: lng });
}
}
Ionic / Cordova App Result:
Native Application Huawei JS API Implementation
In this part of article we are supposed to add javascript based Huawei Map html version into our Native through webview. This part of implementation will be helpful for developer who required very minimal implementation of map.
Make assets/www/map.html file
Add the following HTML code inside map.html file:
Code:
var map;
var mMarker;
var infoWindow;
function initMap() {
const LatLng = { lat: 24.713552, lng: 46.675297 };
const mapOptions = {};
mapOptions.center = LatLng;
mapOptions.zoom = 10;
mapOptions.scaleControl = true;
mapOptions.locationControl= true;
mapOptions.language = "ENG";
map = new HWMapJsSDK.HWMap(
document.getElementById("map"),
mapOptions
);
map.setCenter(LatLng);
mMarker = new HWMapJsSDK.HWMarker({
map: map,
position: LatLng,
zIndex: 10,
label: 'A',
icon: {
opacity: 0.5
}
});
mMarker.addListener('click', () => {
infoWindow.open();
});
infoWindow = new HWMapJsSDK.HWInfoWindow({
map,
position: LatLng,
content: 'This is to info window of marker',
offset: [0, -40],
});
infoWindow.close();
}
Add the webview in your layout:
Code:
< WebView
android:id="@+id/webView_map"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
Update your Activity class to call html file
Code:
class MainActivity : AppCompatActivity() {
lateinit var context: Context
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
context = this
val mWebview = findViewById(R.id.webView_map)
mWebview.webChromeClient = WebChromeClient()
mWebview.webViewClient = WebViewClient()
mWebview.settings.javaScriptEnabled = true
mWebview.settings.setAppCacheEnabled(true)
mWebview.settings.mediaPlaybackRequiresUserGesture = true
mWebview.settings.domStorageEnabled = true
mWebview.loadUrl("file:///android_asset/www/map.html")
}
}
Internet permission:
Don’t forget to add internet permissions in androidmanifest.xml file.
Code:
< uses-permission android:name="android.permission.INTERNET" />
< uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
< uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
Native app Result:
References:
Huawei Map JavaScript API:
https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/javascript-api-introduction-0000001050164063
Complete Ionic JS Map Project:
https://github.com/salmanyaqoob/Ionic-All-HMS-Kits
Conclusion
Huawei Map JavaSript Api will be helpful for JavaScript developers to implement Huawei Map on cross platforms like “Cordova, Ionic, React-Native” and also helpful for the Native developers to implement under his projects. Developers can also able to implement Huawei Maps on websites.
Thank you very much, very helpful.
{
"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"
}
In this comparison article you will read about:
Learn about simple transition animations.
Learn about JUnit testing for URL.
Learn how to record videos using the CameraX library.
Learn to create a simple messaging system with Firebase Realtime Database. ( Bonus Content )
Learn how to add video files to Firebase Storage, call their URLs to Realtime NoSQL of Firebase Database, and use those as our video sources.
How to implement both Exo Player, Widevine, and HMS Video Kit, WisePlayer, and their pros and cons while building both of these projects by their flavor dimensions.
Please also note that the code language that supports this article will be on Kotlin and XML.
Global Setups for our application
What you will need for building this application is listed below:
Hardware Requirements
A computer that can run Android Studio.
An Android phone for debugging.
Software Requirements
Android SDK package
Android Studio 3.X
API level of at least 23
HMS Core (APK) 4.0.1.300 or later or later (Not needed for Exo Player 2.0)
To not hold too much space on our phone let’s start with adding necessary plugins to necessary builds.
Code:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'kotlin-android-extensions'
id 'com.huawei.agconnect'
id 'com.google.gms.google-services'
}
if (getGradle().getStartParameter().getTaskRequests().toString().toLowerCase().contains("gms")) {
apply plugin: 'com.google.gms.google-services'
} else {
apply plugin: 'com.huawei.agconnect'
}
To compare them both let us create a flavor dimension first on our project. Separating GMS (Google Mobile Services) and HMS (Huawei Mobile Services) products.
Code:
flavorDimensions "version"
productFlavors {
hms {
dimension "version"
}
gms {
dimension "version"
}
}
After that add gms and hms as a directory in your src folder. The selected build variant will be highlighted as blue. Note that “java” and “res” must also be added by hand! And don’t worry I’ll show you how to fill them properly.
Binding build features that we’ll use a lot in this project.
Code:
buildFeatures {
dataBinding true
viewBinding = true
}
Let us separate our dependencies now that we have flavor product builds. Note separated implementations are starting with their prefixed names.
Code:
/* ExoPlayer */
def exoP= "2.12.1"
/* Slider Activity */
def roadkill = "2.1.0"
/* CameraX */
def camerax_version = "1.0.0-beta03"
def camerax_extensions = "1.0.0-alpha10"
/* BottomBar */
implementation 'nl.joery.animatedbottombar:library:1.0.9'
/* Firebase */
implementation platform('com.google.firebase:firebase-bom:26.1.0')
implementation 'com.google.firebase:firebase-analytics'
implementation 'com.google.firebase:firebase-database:19.5.1'
implementation 'com.google.firebase:firebase-storage:19.2.1'
implementation 'com.firebaseui:firebase-ui-auth:6.2.0'
implementation 'com.firebaseui:firebase-ui-database:6.2.0'
implementation 'com.firebaseui:firebase-ui-firestore:6.2.0'
implementation 'com.firebaseui:firebase-ui-storage:6.2.0'
/* Simple Video View */
implementation 'com.klinkerapps:simple_videoview:1.2.4'
/* Lottie */
implementation "com.airbnb.android:lottie: 3.4.0"
/*Sliding Activity*/
implementation "com.r0adkll:slidableactivity:$roadkill"
/* Exo */
gmsImplementation "com.google.android.exoplayer:exoplayer:$exoP"
gmsImplementation "com.google.android.exoplayer:exoplayer-core:$exoP"
gmsImplementation "com.google.android.exoplayer:exoplayer-dash:$exoP"
gmsImplementation "com.google.android.exoplayer:exoplayer-ui:$exoP"
gmsImplementation "com.google.android.exoplayer:exoplayer-hls:$exoP"
gmsImplementation "com.google.android.exoplayer:extension-okhttp:$exoP"
/* Huawei */
implementation 'com.huawei.agconnect:agconnect-core:1.4.1.300'
hmsImplementation 'com.huawei.agconnect:agconnect-auth:1.4.1.300'
hmsImplementation 'com.huawei.hms:hwid:5.0.3.302'
hmsImplementation 'com.huawei.hms:videokit-player:1.0.1.300'
// CameraX core library using the camera2 implementation
//noinspection GradleDependency
implementation "androidx.camera:camera-core:${camerax_version}"
//noinspection GradleDependency
implementation "androidx.camera:camera-camera2:${camerax_version}"
// If you want to additionally use the CameraX Lifecycle library
//noinspection GradleDependency
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
// If you want to additionally use the CameraX View class
//noinspection GradleDependency
implementation "androidx.camera:camera-view:${camerax_extensions}"
// If you want to additionally use the CameraX Extensions library
//noinspection GradleDependency
implementation "androidx.camera:camera-extensions:${camerax_extensions}"
// Login
…
Adding Predefined Video URL’s
These URL’s will be our predefined tests that will be used.
XML:
<resources>
<string-array name="data_url">
<item>http://videoplay-mos-dra.dbankcdn.com/P_VT/video_injection/61/v3/519249A7370974110613784576/MP4Mix_H.264_1920x1080_6000_HEAAC1_PVC_NoCut.mp4?accountinfo=Qj3ukBa%2B5OssJ6UBs%2FNh3iJ24kpPHADlWrk80tR3gxSjRYb5YH0Gk7Vv6TMUZcd5Q%2FK%2BEJYB%2BKZvpCwiL007kA%3D%3D%3A20200720094445%3AUTC%2C%2C%2C20200720094445%2C%2C%2C-1%2C1%2C0%2C%2C%2C1%2C%2C%2C%2C1%2C%2C0%2C%2C%2C%2C%2C1%2CEND&GuardEncType=2&contentCode=M2020072015070339800030113000000&spVolumeId=MP2020072015070339800030113000000&server=videocontent-dra.himovie.hicloud.com&protocolType=1&formatPriority=504*%2C204*%2C2</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WeAreGoingOnBullrun.mp4</item>
<item>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4</item>
</string-array>
<string-array name="data_name">
<item>Analytics - HMS Core</item>
<item>Big Buck Bunny</item>
<item>Elephant Dream</item>
<item>For Bigger Blazes</item>
<item>For Bigger Escape</item>
<item>For Bigger Fun</item>
<item>For Bigger Joyrides</item>
<item>For Bigger Meltdowns</item>
<item>Sintel</item>
<item>Subaru Outback On Street And Dirt</item>
<item>Tears of Steel</item>
<item>Volkswagen GTI Review</item>
<item>We Are Going On Bullrun</item>
<item>What care can you get for a grand?</item>
</string-array>
</resources>
Defining Firebase constants
I am skipping all those setups a project part in Firebase part and moving directly to the part that interests you the most, my dear reader. Since we are about to bind videos to each individual user there must be also login processes. I’ll also leave you, my dear reader.
Let us start with getting our logged-in users ID.
Code:
object AppUser {
private var userId = ""
fun setUserId(userId: String) {
if (userId != "")
this.userId = userId
else
this.userId = "dummy"
}
fun getUserId() = userId
}
Firebase Database Helper:
Code:
object FirebaseDbHelper {
fun rootRef() : DatabaseReference {
return FirebaseDatabase.getInstance().reference
}
fun getUser(userID : String) : DatabaseReference {
return FirebaseDatabase.getInstance().reference.child("User").child(userID)
}
fun onDisconnect(db : DatabaseReference) : Task<Void> {
return db.child("onlineStatus").onDisconnect().setValue(ServerValue.TIMESTAMP)
}
fun onConnect(db : DatabaseReference) : Task<Void> {
return db.child("onlineStatus").setValue("true")
}
fun getUserInfo(userID : String) : DatabaseReference {
return FirebaseDatabase.getInstance().reference.child("UserInfo").child(userID)
}
fun getVideoFeed(userID : String) : DatabaseReference {
return FirebaseDatabase.getInstance().reference.child("UserFeed/Video/$userID")
}
fun getVideoFeedItem(userID: String, listId:String):DatabaseReference{
return FirebaseDatabase.getInstance().reference.child("UserFeed/Video/$userID/$listId")
}
fun getShared(): DatabaseReference{
return FirebaseDatabase.getInstance().reference.child("uploads/Shareable")
}
fun getShareItem(listId: String):DatabaseReference{
return FirebaseDatabase.getInstance().reference.child("uploads/Shareable/$listId")
}
fun getUnSharedItem(listId: String): DatabaseReference{
return FirebaseDatabase.getInstance().reference.child("uploads/UnShareable/$listId")
}
fun getPostMessageRootRef() : DatabaseReference {
return FirebaseDatabase.getInstance().reference.child("PostMessage")
}
fun getPostMessageRef(listID : String) : DatabaseReference {
return FirebaseDatabase.getInstance().reference.child("PostMessage/$listID")
}
fun getPostMessageUnderRef(postID : String, listID : String) : DatabaseReference {
return FirebaseDatabase.getInstance().reference.child("PostMessageUnder/$postID/$listID/Message")
}
}
Now that we have helper lets create our global constants:
Code:
class Constants {
companion object {
/* Firebase References */
val fUserInfoDB = FirebaseDbHelper.getUserInfo(AppUser.getUserId())
val fFeedRef = FirebaseDbHelper.getVideoFeed(AppUser.getUserId())
val fSharedRef = FirebaseDbHelper.getShared()
/* Recording Video */
const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS" //"yyyy-MM-dd-HH-mm-ss-SSS"
const val VIDEO_EXTENSION = ".mp4"
var recPath = Environment.getExternalStorageDirectory().path + "/Pictures/ExoVideoReference"
fun getOutputDirectory(context: Context): File {
val appContext = context.applicationContext
val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
File(
recPath
).apply { mkdirs() }
}
return if (mediaDir != null && mediaDir.exists()) mediaDir else appContext.filesDir
}
fun createFile(baseFolder: File, format: String, extension: String) =
File(
baseFolder, SimpleDateFormat(format, Locale.ROOT)
.format(System.currentTimeMillis()) + extension
)
}
}
Let’s create our Data classes to use:
Code:
object DataClass {
data class ProfileVideoDataClass(
val shareStat: String = "",
val like: String = "",
val timeDate: String = "",
val timeMillis: String = "",
val uploaderID: String = "",
val videoUrl: String = "",
val videoName:String = ""
)
data class UploadsShareableDataClass(
val like: String = "",
val timeDate: String = "",
val timeMillis: String = "",
val uploaderID: String = "",
val videoUrl: String = "",
val videoName:String = ""
)
data class PostMessageDataClass(
val comment : String = "",
val comment_lovely : String = "",
val commenter_ID : String = "",
val commenter_image : String = "",
val commenter_name : String = "",
val time : String = "",
val type : String = ""
)
}
Custom URL Test Implementation with JUnit
We need a test based controller to help our users to enter a valid URL address for that let’s begin by creating a new Url Validator class:
Code:
class UrlValidatorHelper : TextWatcher {
internal var isValid = false
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
isValid = isValidUrl(s)
}
companion object {
fun isValidUrl(url: CharSequence?): Boolean {
return url!=null && URLUtil.isValidUrl(url.trim().toString()) && Patterns.WEB_URL.matcher(url).matches()
}
}
}
Now that we have URL helper lets create our test classes under test>>java>>”package_name”>>UrlValidatorTest:
Code:
class UrlValidatorTest {
@Test
fun urlValidator_InvalidCertificate_RunsFalse(){
assertFalse(UrlValidatorHelper.isValidUrl("www.youtube.com/watch?v=Yr8xDSPjII8&list=RDMMYr8xDSPjII8&start_radio=1"))
}
@Test
fun urlValidator_InvalidIdentifier_RunsFalse(){
assertFalse(UrlValidatorHelper.isValidUrl("https://youtube.com/watch?v=Yr8xDSPjII8&list=RDMMYr8xDSPjII8&start_radio=1"))
}
@Test
fun urlValidator_InvalidDomain_RunsFalse(){
assertFalse(UrlValidatorHelper.isValidUrl("https://www.com/watch?v=Yr8xDSPjII8&list=RDMMYr8xDSPjII8&start_radio=1"))
}
@Test
fun urlValidator_InvalidExtension_RunsFalse(){
assertFalse(UrlValidatorHelper.isValidUrl("https://www.youtube/watch?v=Yr8xDSPjII8&list=RDMMYr8xDSPjII8&start_radio=1"))
}
@Test
fun urlValidator_EmptyUrl_RunsFalse(){
assertFalse(UrlValidatorHelper.isValidUrl(""))
}
@Test
fun urlValidator_NullUrl_RunsFalse(){
assertFalse(UrlValidatorHelper.isValidUrl(null))
}
}
------------------------------
END OF PART - I
Interesting.
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
For our scenario, we will have two tables named BookmarkStatus and LikeStatus. BookmarkStatus and LikeStatus tables holds a record for each user’s bookmark/like for the specified object and deletes the record when user remove his/her like or bookmark.
Let’s start with initializing our cloud db object. I will initialize cloud db object once when application started (in SplashScreen) and use it through the application.
companion object {
fun initDb() { AGConnectCloudDB.initialize(ContextProvider.getApplicationContext()) }
}
fun dbGetInstance(){
mCloudDb = AGConnectCloudDB.getInstance()
}
Click to expand...
Click to collapse
Then, create a base viewModel to call certain functions of cloud db instead of calling them in every viewModel.
open class BaseViewModel : ViewModel() {
var mCloudDbZoneWrapper: CloudDbRepository =
CloudDbRepository()
init {
mCloudDbZoneWrapper.createObjectType()
mCloudDbZoneWrapper.openCloudDbZone()
}
}
Click to expand...
Click to collapse
Here is what createObjectType() and openCloudDbZone() functions do.
/*
Create object type
*/
fun createObjectType() {
dbGetInstance()
try {
mCloudDb!!.createObjectType(ObjectTypeInfoHelper.getObjectTypeInfo())
} catch (exception: AGConnectCloudDBException) {
Log.w("CloudDbRepository", exception.errMsg)
}
}
/*
Following method opens cloud db zone with given configs.
*/
fun openCloudDbZone() {
val mConfig: CloudDBZoneConfig = CloudDBZoneConfig(
"YOUR_CLOUD_DB_NAME", CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE,
CloudDBZoneConfig.CloudDBZoneAccessProperty.CLOUDDBZONE_PUBLIC
)
mConfig.persistenceEnabled = true
try {
mCloudDbZone = mCloudDb!!.openCloudDBZone(mConfig, true)
} catch (exception: AGConnectCloudDBException) {
Log.w("CloudDbRepository", exception.errMsg)
}
}
Click to expand...
Click to collapse
Now we have all settings done. All we need to do is calling executeUpsert() and executeDelete() functions properly in related repositories.
private fun bookmarkResult(
snapshot: CloudDBZoneSnapshot<BookmarkStatus>,
bookmark: BookmarkStatus,
triggered: Boolean
) {
val bookmarkStatsCursor: CloudDBZoneObjectList<BookmarkStatus> = snapshot.snapshotObjects
try {
if (bookmarkStatsCursor.hasNext()) {
val bookmarkStats = bookmarkStatsCursor.next()
if (bookmarkStats != null && bookmarkStats.object != null) {
if (triggered) {
//deleteBookmark
val deleteTask = mCloudDBZone.executeDelete(bookmarkStats)
deleteTask.addOnSuccessListener {
Log.w(TAG, "BookmarkDelete success")
bookmarkStatus.postValue(false)
}.addOnFailureListener {
Log.w(TAG, "BookmarkDelete fail" + it.message)
}
} else {
bookmarkStatus.postValue(true)
}
}
} else {
if (triggered) {
//add bookmark
val upsertTask = mCloudDBZone.executeUpsert(bookmark)
upsertTask.addOnSuccessListener {
Log.w(TAG, "BookmarkAdd success")
bookmarkStatus.postValue(true)
}.addOnFailureListener {
Log.w(TAG, "BookmarkDelete fail" + it.message)
}
} else {
bookmarkStatus.postValue(false)
}
}
} catch (exception: Exception) {
}
snapshot.release()
}
Click to expand...
Click to collapse
In this function, triggered parameter is for if user clicked bookmark button or not if clicked then value is true.
Here is the logic;
If user bookmarked the given object (which is queried in another method and passed as a parameter to this method as snapshot) then bookmarkStatsCursor.hasNext() returns true and if not triggered , this means user bookmarked the object and is trying to display bookmark status and all we need to do is using postValue() of the observable property bookmarkStatus and pass the value as true. Let’s say user has a record on BookmarkStatus table and triggered is true then we can say user is trying to remove bookmark of the object. So we need to use executeDelete(bookmark) to delete bookmark from table. With the help of addOnSuccessListener we will post value as false which means user does not have a bookmark on the given object anymore.
If user does not have a bookmark in given object and triggered is false, this means user did not bookmark object and trying to display bookmark status. We will post value as false. If triggered is true then, user is trying to add bookmark to that object. In this situation, we will add a record to the bookmark table using executeUpsert(bookmark) method.
Note that you can use addOnFailureListener to catch errors occurred during adding or deleting functions.
To add or delete records to/from LikeStatus table, you can use same logic with BookmarkStatus table given above.
So, as you can see, it is very simple to implement cloud db in your project and you can apply all CRUD functions simply as demonstrated above
References
https://developer.huawei.com/consum...des/agc-clouddb-introduction-0000001054212760