Related
Hello Folks,
some time ago I did some examination on the diagnostics tool included with every firmware. I did not make any siginficant progress because most of the things I tried reqiuired certificates and signed libraries. However I want to share my thoughts so maybe someone else is able to do smoething with this tool..
The main "vulnerability" in the diagnostics tool is that it tries to load every DLL that is defined as AssemblyPart. Here is the MainPage Constructor
Code:
public MainPage()
{
base.\u002Ector();
this.InitializeComponent();
this.oddtMenuItems = new List<OddtMenuItem>();
AssemblyPartCollection parts = Deployment.Current.Parts;
RadioTechnology radio = this.GetRadio();
bool flag = this.isLTEnabled();
foreach (AssemblyPart assemblyPart in (PresentationFrameworkCollection<AssemblyPart>) parts)
{
try
{
Assembly assembly = Assembly.Load(assemblyPart.Source.Replace(".dll", string.Empty));
OddtMenuItem oddtMenuItem = (OddtMenuItem) Enumerable.FirstOrDefault<object>((IEnumerable<object>) assembly.GetCustomAttributes(typeof (OddtMenuItem), true));
if (oddtMenuItem != null)
{
if (!assembly.Equals((object) Assembly.GetExecutingAssembly()))
{
if (oddtMenuItem.RadioType != RadioTechnology.NA && (oddtMenuItem.RadioType & radio) == RadioTechnology.NA)
oddtMenuItem.setEnabled(false);
if (oddtMenuItem.Title == "Wireless LAN" || oddtMenuItem.Title == "APN Settings")
{
oddtMenuItem.setEnabled(false);
this.oddtMenuItems.Remove(oddtMenuItem);
}
if (oddtMenuItem.Title == "Life Timer" && !flag)
{
oddtMenuItem.setEnabled(false);
this.oddtMenuItems.Remove(oddtMenuItem);
}
this.oddtMenuItems.Add(oddtMenuItem);
}
}
}
catch (Exception ex)
{
}
}
this.PrioritizeMenuItems();
this.MainListBox.DataContext = (object) this.oddtMenuItems;
if ((bool) Application.Current.Resources[(object) "OddtDisclaimerAccepted"])
{
this.ContentGrid.Visibility = Visibility.Visible;
this.DisclaimerGrid.Visibility = Visibility.Collapsed;
this.get_ApplicationBar().set_IsVisible(true);
}
else
{
this.ContentGrid.Visibility = Visibility.Collapsed;
this.DisclaimerGrid.Visibility = Visibility.Visible;
this.get_ApplicationBar().set_IsVisible(false);
}
}
The AssemblyParts are defined in the WMAppManifest.xaml:
Code:
<Deployment xmlns="http://schemas.microsoft.com/client/2007/deployment" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" EntryPointAssembly="OddtApplication" EntryPointType="OddtApplication.App" RuntimeVersion="4.7.50308.0">
<Deployment.Parts>
<AssemblyPart x:Name="OddtApplication" Source="OddtApplication.dll" />
<AssemblyPart x:Name="About" Source="About.dll" />
<AssemblyPart x:Name="Accelerometer" Source="Accelerometer.dll" />
<AssemblyPart x:Name="Als" Source="Als.dll" />
<AssemblyPart x:Name="APN" Source="APN.dll" />
<AssemblyPart x:Name="AudioLoopback" Source="AudioLoopback.dll" />
<AssemblyPart x:Name="Battery" Source="Battery.dll" />
<AssemblyPart x:Name="Bluetooth" Source="Bluetooth.dll" />
<AssemblyPart x:Name="Camera" Source="Camera.dll" />
<AssemblyPart x:Name="CareBattery" Source="CareBattery.dll" />
<AssemblyPart x:Name="CareTestSequence" Source="CareTestSequence.dll" />
<AssemblyPart x:Name="Dtmf" Source="Dtmf.dll" />
<AssemblyPart x:Name="ExampleLibrary" Source="ExampleLibrary.dll" />
<AssemblyPart x:Name="Gyroscope" Source="Gyroscope.dll" />
<AssemblyPart x:Name="HardwareButtons" Source="HardwareButtons.dll" />
<AssemblyPart x:Name="Headset" Source="Headset.dll" />
<AssemblyPart x:Name="LcdWhite" Source="LcdWhite.dll" />
<AssemblyPart x:Name="LedKeypad" Source="LedKeypad.dll" />
<AssemblyPart x:Name="LifeTimer" Source="LifeTimer.dll" />
<AssemblyPart x:Name="LTEBandLock" Source="LTEBandLock.dll" />
<AssemblyPart x:Name="LTETx" Source="LTETx.dll" />
<AssemblyPart x:Name="Magnetometer" Source="Magnetometer.dll" />
<AssemblyPart x:Name="Memory" Source="Memory.dll" />
<AssemblyPart x:Name="Microsoft.Phone.Controls" Source="Microsoft.Phone.Controls.dll" />
<AssemblyPart x:Name="Microsoft.Phone.Controls.Toolkit" Source="Microsoft.Phone.Controls.Toolkit.dll" />
<AssemblyPart x:Name="MMS" Source="MMS.dll" />
<AssemblyPart x:Name="NonLinearNavigationService" Source="NonLinearNavigationService.dll" />
<AssemblyPart x:Name="OddtAttributes" Source="OddtAttributes.dll" />
<AssemblyPart x:Name="PowerSource" Source="PowerSource.dll" />
<AssemblyPart x:Name="Proximity" Source="Proximity.dll" />
<AssemblyPart x:Name="RadioAccess" Source="RadioAccess.dll" />
<AssemblyPart x:Name="Settings" Source="Settings.dll" />
<AssemblyPart x:Name="Speaker" Source="Speaker.dll" />
<AssemblyPart x:Name="Touch" Source="Touch.dll" />
<AssemblyPart x:Name="Vibra" Source="Vibra.dll" />
<AssemblyPart x:Name="WirelessLAN" Source="WirelessLAN.dll" />
<AssemblyPart x:Name="Microsoft.Phone.Media.Extended" Source="Microsoft.Phone.Media.Extended.dll" />
</Deployment.Parts>
</Deployment>
For an MenuItem to appear in the diagnostics app it has to be flagged with the OddtMenuItem-Attribute (or however you call this).
Code:
[AttributeUsage(AttributeTargets.Assembly)]
public class OddtMenuItem : Attribute
{
private OddtMenuItemStatus status;
private string title;
private string description;
private Uri entryPage;
private int priority;
private RadioTechnology radiotype;
public Brush ForegroundColor
{
get
{
if (this.status == OddtMenuItemStatus.Alpha)
return (Brush) new SolidColorBrush(Colors.Gray);
if ((Color) ((FrameworkElement) new PhoneApplicationPage()).Resources[(object) "PhoneBackgroundColor"] == Color.FromArgb(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue))
return (Brush) new SolidColorBrush(Colors.Black);
else
return (Brush) new SolidColorBrush(Colors.White);
}
}
public bool IsEnabled
{
get
{
return this.status > OddtMenuItemStatus.Alpha;
}
}
public RadioTechnology RadioType
{
get
{
return this.radiotype;
}
}
public Visibility Visibility
{
get
{
return this.status == (OddtMenuItemStatus.Alpha | OddtMenuItemStatus.Beta) ? Visibility.Visible : Visibility.Collapsed;
}
}
public string Title
{
get
{
return this.title;
}
}
public string Description
{
get
{
return this.description;
}
}
public Uri EntryPage
{
get
{
return this.entryPage;
}
}
public int Priority
{
get
{
return this.priority;
}
set
{
this.priority = value;
}
}
public OddtMenuItem(OddtMenuItemStatus status, string title, string description, string location, RadioTechnology radiotype = RadioTechnology.NA)
{
this.status = status;
this.title = status != OddtMenuItemStatus.Beta ? title : string.Format("{{{0}}}", (object) title);
this.description = description;
this.entryPage = new Uri(location, UriKind.Relative);
this.priority = 99999;
this.radiotype = radiotype;
}
public void setEnabled(bool enable)
{
if (enable)
return;
this.status = OddtMenuItemStatus.Alpha;
}
}
I think that we can execute code with high Level privileges as soon as we are able to manipulate the WMAppManifest.xaml and copy a DLL into the App folder. The main Problem is that every Manipulation of the Folder or one of ist files invalidates the signature.
Regards
Chris
{
"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 will develop Music Station android app for Huawei Vision S (Smart TV) devices. Huawei Vision S is a brand new large screen category and important part of Huawei's "1+8+N" full-scenario services and Huawei Developer Ecosystem. Since, Huawei Vision S system architecture supports AOSP project framework, we used Leanback Library which offers extensive features for large screens to develop our user experience.
Why an app for Huawei Vision S?
Large screen offers better visibility and enhanced user experience. Due to Covid-19 lockdown, Smart TV has grown to include over 80% more users than it had this time last year. Total distribution of usage for TV is increasing rapidly. As a result of this, total number of TV apps has jumped dramatically including educational and entertainment apps.
Designing App for Huawei Vision S
While designing an app for Huawei Vision S, we have to keep following key points in our mind:
Build Layout for TV: We must design landscape orientation layout that allows users to easily see the screen 10 feet away from the TV.
Management Controller: Our app must support arrow keys and handle offline controllers as well as inputs from multiple controllers.
For this article, we implemented Leanback Library which offers amazing and interactive user experience for apps such as Audio/Video players and so on.
Pre-Requisites
Before getting started, following are the requirements:
Android Studio (During this tutorial, we used version 4.1.1)
Android SDK 24 or later
Huawei Vision S for testing
Development
Following are the major steps of development for this article:
Step 1: Add Dependencies & Permissions
1.1: Add the following dependencies in the app level build.gradle file:
Java:
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
// TV Libs
implementation 'androidx.leanback:leanback:1.0.0'
implementation 'androidx.leanback:leanback-preference:1.0.0'
// General
implementation 'org.greenrobot:eventbus:3.2.0'
implementation 'com.google.code.gson:gson:2.8.7'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.nabinbhandari.android:permissions:3.8'
// Animation
implementation 'com.airbnb.android:lottie:3.7.0'
implementation 'com.gauravk.audiovisualizer:audiovisualizer:0.9.2'
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation ('com.github.bumptech.glide:okhttp3-integration:4.12.0'){
exclude group: 'glide-parent'
}
}
1.2: We are developing this app only for TV. So, we will disable the Touch_Screen requirements and enable the Leanback. For the audio visualizer, we need Record_Audio permission. Add the following permissions and features tag in the AndroidManifest.xml:
Java:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="true" />
<uses-feature
android:name="android.hardware.microphone"
android:required="false" />
1.3: Huawei Vision S supports Leanback Library but does not supports Leanback Launcher. So, we added the following tag in the SplashActivity inside the AndroidManifest.xml:
Code:
<activity android:name=".activities.SplashActivity"
android:screenOrientation="landscape"
android:banner="@drawable/app_icon"
android:icon="@drawable/app_icon"
android:label="@string/app_name"
android:launchMode="singleTop"
android:logo="@drawable/app_icon"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<!-- HUAWEI Vision S only support CATEGORY_LAUNCHER -->
<category android:name="android.intent.category.LAUNCHER" />
<!-- ADD the below line only if you want to release the same code on Google Play -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
1.4: The banner icon is required when we are developing apps for TV. The size for Huawei Vision S banner icon is 496x280 which must be added in the drawable folder.
Step 2: Generating Supported Language JSON
Since our main goal is playing Music, we restricted data source and generated a JSON file locally to avoid API creation and API calling. In real world scenario, an API can be developed or can be used to get the real-time data.
Step 3: Building Layout
3.1: The most important layout of our application is of PlayerActivity. Add the following activity_player.xml layout file in the layout folder of the res. We developed this layout to enhance user experience and add custom views like Audio Visualizer. The layout has two main sub-layouts, Player Content View and Error View.
XML:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/main_background"
android:keepScreenOn="true">
<ImageView
android:id="@+id/imgBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/main_background" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/splashAnimation"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:lottie_autoPlay="false"
app:lottie_progress="53"
app:lottie_rawRes="@raw/splash_animation" />
<com.gauravk.audiovisualizer.visualizer.CircleLineVisualizer
android:id="@+id/blastVisualizer"
android:layout_width="match_parent"
android:layout_height="match_parent"
custom:avColor="@color/light_red"
custom:avDensity="0.8"
custom:avSpeed="normal"
custom:avType="fill" />
<ImageView
android:id="@+id/imgOverlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.5"
android:background="@android:color/black" />
<ProgressBar
android:id="@+id/progressBarLoader"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerInParent="true"
android:indeterminate="true"
android:indeterminateTint="@color/colorPrimaryDark"
android:indeterminateTintMode="src_atop"
android:visibility="gone" />
<RelativeLayout
android:id="@+id/rlContentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:paddingStart="50dp"
android:paddingEnd="50dp">
<ImageView
android:id="@+id/imgIcon"
android:layout_width="80dp"
android:layout_height="80dp" />
<TextView
android:id="@+id/txtSongName"
style="@style/TextViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/imgIcon"
android:layout_marginTop="5dp"
android:textSize="@dimen/title_text_size" />
<TextView
android:id="@+id/txtArtistName"
style="@style/TextViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/txtSongName"
android:layout_marginTop="5dp"
android:textSize="@dimen/artist_text_size" />
<TextView
android:id="@+id/txtCategoryName"
style="@style/TextViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/txtArtistName"
android:layout_marginTop="5dp"
android:textSize="@dimen/category_text_size" />
<ImageButton
android:id="@+id/btnPlayPause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txtCategoryName"
android:layout_marginTop="50dp"
android:background="@drawable/ui_selector_bg"
android:focusable="true"
android:src="@drawable/lb_ic_play" />
<SeekBar
android:id="@+id/seekBarAudio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/btnPlayPause"
android:layout_marginTop="10dp"
android:background="@drawable/ui_selector_bg"
android:colorControlActivated="@color/white"
android:focusable="true"
android:progressTint="@color/white"
android:thumbTint="@color/white" />
<TextView
android:id="@+id/txtTotalDuration"
style="@style/TextViewStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/seekBarAudio"
android:layout_alignEnd="@+id/seekBarAudio"
android:layout_marginTop="5dp"
android:text=" / -"
android:textSize="@dimen/time_text_size" />
<TextView
android:id="@+id/txtCurrentTime"
style="@style/TextViewStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/seekBarAudio"
android:layout_marginTop="5dp"
android:layout_toStartOf="@+id/txtTotalDuration"
android:text="-"
android:textSize="@dimen/time_text_size" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rlErrorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:visibility="gone">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<ImageView
android:id="@+id/imgErrorLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:src="@drawable/lb_ic_sad_cloud" />
<TextView
android:id="@+id/txtError"
style="@style/TextViewStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/imgErrorLoading"
android:layout_centerHorizontal="true"
android:text="@string/error_message"
android:textSize="@dimen/time_text_size" />
<Button
android:id="@+id/btnDismiss"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/txtError"
android:layout_centerHorizontal="true"
android:layout_marginTop="30dp"
android:focusable="true"
android:text="@string/dismiss_error" />
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>
3.2: In this article, we used Lottie animation in the SplashActivity for better user experience inside the following activity_splash.xml.
XML:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/colorPrimaryDark"
tools:context=".activities.SplashActivity">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/splashAnimation"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:lottie_autoPlay="true"
app:lottie_repeatCount="1"
app:lottie_rawRes="@raw/splash_animation" />
<ImageView
android:id="@+id/imgAppIcon"
android:src="@drawable/img_music_station_logo"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:layout_marginBottom="40dp"
android:scaleX="0.8"
android:scaleY="0.8"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
Step 4: Adding JAVA Classes
4.1: We extended the MainFragment class from BrowseFragment which is Leanback home layout component. This view handles the landing screen containing interactive side menu, main content area with different cards and navigation between them.
Java:
public class MainFragment extends BrowseFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
prepareBackgroundManager();
setupUIElements();
loadRows();
setupEventListeners();
}
private void loadRows() {
ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
CardPresenter cardPresenter = new CardPresenter();
int i;
for (i = 0; i < DataUtil.getData(getActivity()).size(); i++) {
CategoryModel categoryModel = DataUtil.getData(getActivity()).get(i);
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter);
for (int j = 0; j < categoryModel.getCategorySongs().size(); j++) {
listRowAdapter.add(categoryModel.getCategorySongs().get(j));
}
HeaderItem header = new HeaderItem(i, categoryModel.getCategoryName());
rowsAdapter.add(new ListRow(header, listRowAdapter));
}
setAdapter(rowsAdapter);
}
private void prepareBackgroundManager() {
BackgroundManager mBackgroundManager = BackgroundManager.getInstance(getActivity());
mBackgroundManager.attach(getActivity().getWindow());
mBackgroundManager.setColor(getResources().getColor(R.color.main_background));
DisplayMetrics mMetrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
}
private void setupUIElements() {
setTitle(getString(R.string.app_name));
setHeadersState(HEADERS_ENABLED);
setHeadersTransitionOnBackEnabled(true);
setBrandColor(ContextCompat.getColor(getActivity(), R.color.colorPrimaryDark));
setSearchAffordanceColor(ContextCompat.getColor(getActivity(), R.color.colorPrimary));
}
private void setupEventListeners() {
setOnItemViewClickedListener(new ItemViewClickedListener());
}
private final class ItemViewClickedListener implements OnItemViewClickedListener {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
if (item instanceof Song) {
Song song = (Song) item;
Intent intent = new Intent(getActivity(), PlayerActivity.class);
intent.putExtra(DataUtil.SONG_DETAIL, song);
getActivity().startActivity(intent);
}
}
}
}
4.2: Cards are displayed using CardPresenter which is extended by Presenter from the Leanback library.
Java:
public class CardPresenter extends Presenter {
private static final int CARD_WIDTH = 313;
private static final int CARD_HEIGHT = 176;
private static int sSelectedBackgroundColor;
private static int sDefaultBackgroundColor;
private static void updateCardBackgroundColor(ImageCardView view, boolean selected) {
int color = selected ? sSelectedBackgroundColor : sDefaultBackgroundColor;
view.setBackgroundColor(color);
view.findViewById(R.id.info_field).setBackgroundColor(color);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
sDefaultBackgroundColor = ContextCompat.getColor(parent.getContext(), R.color.background);
sSelectedBackgroundColor = ContextCompat.getColor(parent.getContext(), R.color.colorPrimaryDark);
ImageCardView cardView =
new ImageCardView(parent.getContext()) {
@Override
public void setSelected(boolean selected) {
updateCardBackgroundColor(this, selected);
super.setSelected(selected);
}
};
cardView.setFocusable(true);
cardView.setFocusableInTouchMode(true);
updateCardBackgroundColor(cardView, false);
return new ViewHolder(cardView);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, Object item) {
Song song = (Song) item;
ImageCardView cardView = (ImageCardView) viewHolder.view;
cardView.setTitleText(song.getSongName());
cardView.setContentText(song.getArtistName());
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT);
Glide.with(viewHolder.view.getContext())
.load(song.getImageURL())
.centerCrop()
.placeholder(R.drawable.app_icon)
.error(R.drawable.app_icon)
.into(cardView.getMainImageView());
}
@Override
public void onUnbindViewHolder(ViewHolder viewHolder) {
ImageCardView cardView = (ImageCardView) viewHolder.view;
cardView.setBadgeImage(null);
cardView.setMainImage(null);
}
}
4.3: Whenever user click on any Card Item, PlayerActivity is opened. Following are some of the important functions of the PlayerActivity.java. Please refer to the github link for complete code of this class.
Java:
private void handlePlayer(){
if (currentState == MediaPlayerHolder.PlayerState.PLAYING) {
pauseSong();
} else if (currentState == MediaPlayerHolder.PlayerState.PAUSED ||
currentState == MediaPlayerHolder.PlayerState.COMPLETED ||
currentState == MediaPlayerHolder.PlayerState.RESET) {
playSong();
}
}
private void setUI() {
if (song != null) {
txtSongName.setText(song.getSongName());
txtArtistName.setText(song.getArtistName());
txtCategoryName.setText(song.getCategoryName());
Glide.with(this)
.load(song.getImageURL())
.centerCrop()
.circleCrop()
.placeholder(R.drawable.app_icon)
.error(R.drawable.app_icon)
.into(imgIcon);
}
setupSeekBar();
}
private void pauseSong() {
EventBus.getDefault().post(new LocalEventFromMainActivity.PausePlayback());
}
private void playSong() {
EventBus.getDefault().post(new LocalEventFromMainActivity.StartPlayback());
}
private void resetSong() {
EventBus.getDefault().post(new LocalEventFromMainActivity.ResetPlayback());
}
public void log(StringBuffer formattedMessage) {
Log.d(PlayerActivity.class.getSimpleName(), String.format("log: %s", formattedMessage));
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(LocalEventFromMediaPlayerHolder.UpdateLog event) {
log(event.formattedMessage);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(LocalEventFromMediaPlayerHolder.PlaybackDuration event) {
seekBarAudio.setMax(event.duration);
setTotalDuration(event.duration);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(LocalEventFromMediaPlayerHolder.PlaybackPosition event) {
if (!isUserSeeking) {
seekBarAudio.setProgress(event.position, true);
updateProgressTime(event.position);
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(LocalEventFromMediaPlayerHolder.StateChanged event) {
hideLoader();
currentState = event.currentState;
switch (event.currentState) {
case PLAYING:
btnPlayPause.setImageResource(R.drawable.lb_ic_pause);
if (mMediaPlayerHolder.getAudioSessionId() != -1 && blastVisualizer != null){
blastVisualizer.setAudioSessionId(mMediaPlayerHolder.getAudioSessionId());
blastVisualizer.show();
}
break;
case PAUSED:
case RESET:
case COMPLETED:
btnPlayPause.setImageResource(R.drawable.lb_ic_play);
if (blastVisualizer != null){
blastVisualizer.hide();
}
break;
case ERROR:
showError();
break;
}
}
private void showError() {
if(song != null){
String title = song.getSongName();
String artist = song.getArtistName();
String songName = "Unable to play " + title + " (" + artist + ")";
txtError.setText(songName);
}
rlErrorLayout.setVisibility(View.VISIBLE);
btnDismiss.requestFocus();
}
private void setTotalDuration(int duration) {
long totalSecs = TimeUnit.MILLISECONDS.toSeconds(duration);
long hours = totalSecs / 3600;
long minutes = (totalSecs % 3600) / 60;
long seconds = totalSecs % 60;
String totalDuration = String.format(Locale.ENGLISH, "%02d:%02d:%02d", hours, minutes, seconds);
if (hours == 0) {
totalDuration = String.format(Locale.ENGLISH, "%02d:%02d", minutes, seconds);
}
String text = " / " + totalDuration;
txtTotalDuration.setText(text);
}
private void updateProgressTime(int position){
long currentSecs = TimeUnit.MILLISECONDS.toSeconds(position);
long hours = currentSecs / 3600;
long minutes = (currentSecs % 3600) / 60;
long seconds = currentSecs % 60;
String currentDuration = String.format(Locale.ENGLISH, "%02d:%02d:%02d", hours, minutes, seconds);
if (hours == 0) {
currentDuration = String.format(Locale.ENGLISH, "%02d:%02d", minutes, seconds);
}
txtCurrentTime.setText(currentDuration);
}
4.4: We used EventBus to asynchronously notify the PlayerActivity UI changes. The MediaPlayerHolder.java class handles the MediaPlayer states and manages the functionalities like playing, pause and seekbar position updates. Please refer to the github link for complete code of this class.
Java:
public int getAudioSessionId(){
if(mMediaPlayer != null){
return mMediaPlayer.getAudioSessionId();
} else
return -1;
}
public void release() {
logToUI("release() and mMediaPlayer = null");
mMediaPlayer.release();
EventBus.getDefault().unregister(this);
}
public void stop() {
logToUI("stop() and mMediaPlayer = null");
mMediaPlayer.stop();
}
public void play() {
if (!mMediaPlayer.isPlaying()) {
logToUI(String.format("start() %s", urlPath));
mMediaPlayer.start();
startUpdatingSeekbarWithPlaybackProgress();
EventBus.getDefault()
.post(new LocalEventFromMediaPlayerHolder.StateChanged(PlayerState.PLAYING));
}
}
public void pause() {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
logToUI("pause()");
EventBus.getDefault()
.post(new LocalEventFromMediaPlayerHolder.StateChanged(PlayerState.PAUSED));
}
}
public void reset() {
logToUI("reset()");
mMediaPlayer.reset();
load(urlPath);
stopUpdatingSeekbarWithPlaybackProgress(true);
EventBus.getDefault()
.post(new LocalEventFromMediaPlayerHolder.StateChanged(PlayerState.RESET));
}
public void load(String url) {
this.urlPath = url.replaceAll(" ", "%20");
if (mMediaPlayer != null) {
try {
mMediaPlayer.setAudioAttributes(
new AudioAttributes
.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build());
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer player) {
initSeekbar();
play();
}
});
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
EventBus.getDefault()
.post(new LocalEventFromMediaPlayerHolder.StateChanged(PlayerState.ERROR));
return false;
}
});
mMediaPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
@Override
public void onSeekComplete(MediaPlayer arg0) {
EventBus.getDefault().post(new LocalEventFromMediaPlayerHolder.StateChanged(PlayerState.PLAYING));
SystemClock.sleep(200);
mMediaPlayer.start();
}
});
logToUI("load() {1. setDataSource}");
mMediaPlayer.setDataSource(urlPath);
logToUI("load() {2. prepare}");
mMediaPlayer.prepareAsync();
} catch (Exception e) {
EventBus.getDefault().post(new LocalEventFromMediaPlayerHolder.StateChanged(PlayerState.ERROR));
logToUI(e.toString());
}
}
}
public void seekTo(int duration) {
logToUI(String.format(Locale.ENGLISH, "seekTo() %d ms", duration));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
mMediaPlayer.seekTo(duration, MediaPlayer.SEEK_CLOSEST);
else
mMediaPlayer.seekTo(duration);
}
private void stopUpdatingSeekbarWithPlaybackProgress(boolean resetUIPlaybackPosition) {
if (mExecutor != null) {
mExecutor.shutdownNow();
}
mExecutor = null;
mSeekbarProgressUpdateTask = null;
if (resetUIPlaybackPosition) {
EventBus.getDefault().post(new LocalEventFromMediaPlayerHolder.PlaybackPosition(0));
}
}
private void startUpdatingSeekbarWithPlaybackProgress() {
// Setup a recurring task to sync the mMediaPlayer position with the Seekbar.
if (mExecutor == null) {
mExecutor = Executors.newSingleThreadScheduledExecutor();
}
if (mSeekbarProgressUpdateTask == null) {
mSeekbarProgressUpdateTask = new Runnable() {
@Override
public void run() {
if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
int currentPosition = mMediaPlayer.getCurrentPosition();
EventBus.getDefault().post(
new LocalEventFromMediaPlayerHolder.PlaybackPosition(
currentPosition));
}
}
};
}
mExecutor.scheduleAtFixedRate(
mSeekbarProgressUpdateTask,
0,
SEEKBAR_REFRESH_INTERVAL_MS,
TimeUnit.MILLISECONDS
);
}
public void initSeekbar() {
// Set the duration.
final int duration = mMediaPlayer.getDuration();
EventBus.getDefault().post(new LocalEventFromMediaPlayerHolder.PlaybackDuration(duration));
logToUI(String.format(Locale.ENGLISH, "setting seekbar max %d sec", TimeUnit.MILLISECONDS.toSeconds(duration)));
}
When user click Select button on the Remote Control of Huawei Vision S on any item, the player view is opened and MediaPlayer start loading the song url. Once the song is loaded, it starts playing and the icon of Play changes to Pause. The Audio Visualizer takes AudioSessionId to sync with audio song. By default, seekbar is selected for the user to skip the songs using Right and Left buttons of the Remote Control. The duration of the song updates based on seekbar position.
Step 5: Run the application
We have added all the required code. Now, just build the project, run the application and test on Huawei Vision S.
Conclusion
Using Leanback, developers can develop beautifully crafted android applications with amazing UI/UX experience for Huawei Vision S. They can also enhance their user engagement and behavior. Combining different Huawei Kits supported by Vision S like Account or IAP can yield amazing results.
Tips and Tricks
You must use default Launcher if you are developing app for Huawei Vision S.
Leanback Launcher is not supported by Huawei Vision S.
If you have same code base for Mobile and Vision S devices, you can use TVUtils class to check at run-time about the device and offer functionalities based on it.
Make sure to add all the permissions like RECORD_AUDIO, INTERNET.
Make sure to add run-time permissions check. In this article, we used 3rd party Permission Check library with custom Dialog if user deny any of the required permission.
Always use animation libraries like Lottie or ViewAnimator to enhance UI/UX in your application.
We used AudioVisualizer library to bring Music feel on our Player UI.
References
Android TV Documentation:
https://developer.android.com/training/tv/start
https://developer.android.com/training/tv/start/layouts
https://developer.android.com/training/tv/start/controllers
https://developer.android.com/training/tv/start/navigation
Lottie Android Documentation:
http://airbnb.io/lottie/#/android
Github Code Link:
https://github.com/yasirtahir/MusicStationTV
Original Source
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Introduction
Hi everyone, In this article, we’ll explore how to develop a download manager app using the Huawei Network Kit. And, we’ll use Kotlin as a programming language in Android Studio.
Huawei Network Kit
Network Kit provides us to upload or download files with additional features such as multithreaded, concurrent, resumable uploads and downloads. Also, it allows us to perform our network operations quickly and safely. It provides a powerful interacting with Rest APIs and sending synchronous and asynchronous network requests with annotated parameters. Finally, we can use it with other Huawei kits such as hQUIC Kit and Wireless Kit to get faster network traffic.
If you want to learn how to use Network Kit with Rest APIs, you can check my article about it.
Download Manager — Sample App
In this project, we’re going to develop a download manager app that helps users download files quickly and reliably to their devices.
Key features:
Start, Pause, Resume or Cancel downloads.
Enable or Disable Sliced Download.
Set’s the speed limit for downloading a file.
Calculate downloaded size/total file size.
Calculate and display download speed.
Check the progress in the download bar.
Support HTTP and HTTPS protocols.
Copy URL from clipboard easily
We started a download task. Then, we paused and resumed it. When the download is finished, it showed a snackbar to notify us.
Setup the Project
We’re not going to go into the details of integrating Huawei HMS Core into a project. You can follow the instructions to integrate HMS Core into your project via official docs or codelab. After integrating HMS Core, let’s add the necessary dependencies.
Add the necessary dependencies to build.gradle (app level).
Java:
dependencies {
...
// HMS Network Kit
implementation 'com.huawei.hms:filemanager:5.0.3.300'
// For runtime permission
implementation 'androidx.activity:activity-ktx:1.2.3'
implementation 'androidx.fragment:fragment-ktx:1.3.4'
...
}
Let’s add the necessary permissions to our manifest.
XML:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.huawei.networkkitsample">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
We added the Internet Permission to access the Internet and the storage permissions to read and write data to the device memory. Also, we will dynamically request the permissions at runtime for storage permissions on devices that runs Android 6.0 (API Level 23) or higher.
Configure the AndroidManifest file to use clear text traffic
If you try to download a file from an HTTP URL on Android 9.0 (API level 28) or higher, you’ll get an error like this:
Code:
ErrorCodeFromException errorcode from resclient: 10000802,message:CLEARTEXT communication to ipv4.download.thinkbroadband.com(your url) not permitted by network security policy
Because cleartext support is disabled by default on Android 9.0 or higher. You should add the android:usesClearTextTraffic="true" flag in the AndroidManifest.xml file. If you don’t want to enable it for all URLs, you can create a network security config file. If you are only working with HTTPS files, you don’t need to add this flag.
XML:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.huawei.networkkitsample">
...
<application
...
android:usesCleartextTraffic="true"
...
</application>
</manifest>
Layout File
activity_main.xml is the only layout file in our project. There are:
A TextInputEditText to enter URL,
Four buttons to control the download process,
A button to paste URL to the TextInputEditText,
A progress bar to show download status,
A seekbar to adjust download speed limit,
A checkbox to enable or disable the “Slide Download” feature,
TextViews to show various information.
activity_main.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main_constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<Button
android:id="@+id/startDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Start"
app:layout_constraintEnd_toStartOf="@+id/pauseDownload_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/enableSliced_checkBox" />
<ProgressBar
android:id="@+id/downloadProgress_progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:progressBackgroundTint="@color/design_default_color_primary_variant"
android:progressTint="@color/design_default_color_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/percentProgress_textView" />
<TextView
android:id="@+id/percentProgress_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="0%"
app:layout_constraintStart_toStartOf="@+id/downloadProgress_progressBar"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />
<TextView
android:id="@+id/finishedSize_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="0"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintStart_toEndOf="@+id/percentProgress_textView"
tools:text="2.5" />
<TextView
android:id="@+id/sizeSeparator_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="/"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintStart_toEndOf="@+id/finishedSize_textView" />
<TextView
android:id="@+id/totalSize_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="0"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintStart_toEndOf="@+id/sizeSeparator_textView"
tools:text="29.6 MB" />
<SeekBar
android:id="@+id/speedLimit_seekBar"
style="@style/Widget.AppCompat.SeekBar.Discrete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:max="7"
android:progress="7"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fixSpeedLimit_textView" />
<TextView
android:id="@+id/fixSpeedLimit_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:text="Download Speed Limit:"
app:layout_constraintStart_toStartOf="@+id/speedLimit_seekBar"
app:layout_constraintTop_toBottomOf="@+id/remainingTime_textView" />
<TextView
android:id="@+id/speedLimit_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Limitless"
app:layout_constraintBottom_toBottomOf="@+id/fixSpeedLimit_textView"
app:layout_constraintStart_toEndOf="@+id/fixSpeedLimit_textView" />
<TextView
android:id="@+id/currentSpeed_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0 kB/s"
app:layout_constraintBottom_toTopOf="@+id/downloadProgress_progressBar"
app:layout_constraintEnd_toEndOf="@+id/downloadProgress_progressBar"
tools:text="912 kB/s" />
<Button
android:id="@+id/pauseDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pause"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/startDownload_button"
app:layout_constraintTop_toTopOf="@+id/startDownload_button" />
<Button
android:id="@+id/resumeDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Resume"
app:layout_constraintEnd_toEndOf="@+id/startDownload_button"
app:layout_constraintStart_toStartOf="@+id/startDownload_button"
app:layout_constraintTop_toBottomOf="@+id/startDownload_button" />
<Button
android:id="@+id/cancelDownload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Cancel"
app:layout_constraintEnd_toEndOf="@+id/pauseDownload_button"
app:layout_constraintStart_toStartOf="@+id/pauseDownload_button"
app:layout_constraintTop_toBottomOf="@+id/pauseDownload_button" />
<TextView
android:id="@+id/remainingTime_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0s left"
app:layout_constraintStart_toStartOf="@+id/downloadProgress_progressBar"
app:layout_constraintTop_toBottomOf="@+id/downloadProgress_progressBar" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/pasteClipboard_imageButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/url_textInputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="URL"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/pasteClipboard_imageButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
android:background="@android:color/transparent"
android:scaleType="fitXY"
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textInputLayout"
app:srcCompat="@drawable/ic_paste_content" />
<CheckBox
android:id="@+id/enableSliced_checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:checked="true"
android:text="Enable Slice Download"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/speedLimit_seekBar" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
Let’s interpret some of the functions on this page.
onCreate() - Firstly we used viewBinding instead of findViewById. It generates a binding class for each XML layout file present in that module. With the instance of a binding class, we can access the view hierarchy with type and null safety.
Then, we initialized the ButtonClickListeners and the ViewChangeListeners. And we create a FileRequestCallback object. We’ll go into the details of this object later.
startDownloadButton() - When the user presses the start download button, it requests permissions at runtime. If the user allows accessing device memory, it will start the download process.
startDownload() - First, we check the downloadManager is initialized or not. Then, we check if there is a download task or not. getRequestStatus function provides us the result status as INIT, PROCESS, PAUSE and, INVALID.
Code:
If auto-import is active in your Android Studio, It can import the wrong package for the Result Status. Please make sure to import the "com.huawei.hms.network.file.api.Result" package.
The Builder helps us to create a DownloadManager object. We give a name to our task. If you plan to use the multiple download feature, please be careful to give different names to your download managers.
The DownloadManagerBuilder helps us to create a DownloadManager object. We give a tag to our task. In our app, we only allow single downloading to make it simple. If you plan to use the multiple download feature, please be careful to give different tags to your download managers.
When creating a download request, we need a file path to save our file and a URL to download. Also, we can set a speed limit or enable the slice download.
Code:
Currently, you can only set the speed limit for downloading a file. The speed limit value ranges from 1 B/s to 1 GB/s. speedLimit() takes a variable of the type INT as a byte value.
You can enable or disable the sliced download.
Code:
Sliced Download: It slices the file into multiple small chunks and downloads them in parallel.
Finally, we start an asynchronous request with downloadManager.start() command. It takes the getRequest and the fileRequestCallback.
FileRequestCallback object contains four callback methods: onStart, onProgress, onSuccess and onException.
onStart -> It will be called when the file download starts. We take the startTime to calculate the remaining download time here.
onProgress -> It will be called when the file download progress changes. We can change the progress status here.
Code:
These methods run asynchronously. If we want to update the UI, we should change our thread to the UI thread using the runOnUiThread methods.
onSuccess -> It will be called when file download is completed. We show a snackbar to the user after the file download completes here.
onException -> It will be called when an exception occurs.
Code:
onException also is triggered when the download is paused or resumed. If the exception message contains the "10042002" number, it is paused, if it contains the "10042003", it is canceled.
MainActivity.kt
Java:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var downloadManager: DownloadManager
private lateinit var getRequest: GetRequest
private lateinit var fileRequestCallback: FileRequestCallback
private val TAG = "MainActivity"
private var downloadURL = "http://ipv4.download.thinkbroadband.com/20MB.zip"
private var downloadSpeedLimit: Int = 0
private var startTime: Long = 0L
private var isEnableSlicedDownload = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.urlTextInputEditText.setText(downloadURL)
initButtonClickListeners()
initViewChangeListeners()
fileRequestCallback = object : FileRequestCallback() {
override fun onStart(getRequest: GetRequest): GetRequest {
startTime = System.nanoTime()
return getRequest
}
override fun onProgress(getRequest: GetRequest, progress: Progress) {
runOnUiThread {
binding.downloadProgressProgressBar.progress = progress.progress
binding.percentProgressTextView.text = "${progress.progress}%"
convertByteToMb(progress.totalSize)?.let {
binding.totalSizeTextView.text = "$it MB"
}
convertByteToMb(progress.finishedSize)?.let {
binding.finishedSizeTextView.text = it
}
showCurrentDownloadSpeed(progress.speed)
showRemainingTime(progress)
}
}
override fun onSuccess(response: Response<GetRequest, File, Closeable>?) {
if (response?.content != null) {
runOnUiThread {
binding.downloadProgressProgressBar.progress = 100
binding.percentProgressTextView.text = "100%"
binding.remainingTimeTextView.text = "0s left"
convertByteToMb(response.content.length())?.let {
binding.finishedSizeTextView.text = it
binding.totalSizeTextView.text = "$it MB"
}
showSnackBar(binding.mainConstraintLayout, "Download Completed")
}
}
}
override fun onException(
getRequest: GetRequest?,
exception: NetworkException?,
response: Response<GetRequest, File, Closeable>?
) {
if (exception != null) {
val pauseTaskValue = "10042002"
val cancelTaskValue = "10042003"
val errorMessage = exception.message
errorMessage?.let {
if (!it.contains(pauseTaskValue) && !it.contains(cancelTaskValue)) {
Log.e(TAG, "Error Message:$it")
exception.cause?.let { throwable ->
runOnUiThread {
Toast.makeText(
[email protected],
throwable.message,
Toast.LENGTH_SHORT
)
.show()
}
}
}
}
}
}
}
}
private fun initViewChangeListeners() {
binding.speedLimitSeekBar.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
downloadSpeedLimit = calculateSpeedLimitAsByte(progress)
showDownloadSpeedLimit(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
}
})
binding.enableSlicedCheckBox.setOnCheckedChangeListener { _, isChecked ->
isEnableSlicedDownload = isChecked
}
}
private fun initButtonClickListeners() {
binding.startDownloadButton.setOnClickListener {
activityResultLauncher.launch(
arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
)
}
binding.pauseDownloadButton.setOnClickListener {
if (isDownloadManagerInitialized().not()) [email protected]
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PROCESS -> {
downloadManager.pauseRequest(getRequest.id)
}
else -> {
Toast.makeText(this, "No valid download request", Toast.LENGTH_SHORT).show()
}
}
}
binding.resumeDownloadButton.setOnClickListener {
if (isDownloadManagerInitialized().not()) [email protected]
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PAUSE -> {
downloadManager.resumeRequest(getRequest, fileRequestCallback)
}
else -> {
Toast.makeText(this, "No download process", Toast.LENGTH_SHORT).show()
}
}
}
binding.cancelDownloadButton.setOnClickListener {
if (isDownloadManagerInitialized().not()) [email protected]
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PROCESS -> {
downloadManager.cancelRequest(getRequest.id)
clearAllViews()
}
Result.STATUS.PAUSE -> {
downloadManager.cancelRequest(getRequest.id)
clearAllViews()
}
else -> {
Toast.makeText(this, "No valid download request", Toast.LENGTH_SHORT).show()
}
}
}
binding.pasteClipboardImageButton.setOnClickListener {
pasteClipboardData()
}
}
private val activityResultLauncher =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
)
{ permissions ->
val allGranted = permissions.entries.map {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
checkSelfPermission(it.key)
} else {
true
}
}.map { it == PackageManager.PERMISSION_GRANTED }.find { !it } ?: true
if (!allGranted) {
Toast.makeText(this, "Permission are not granted", Toast.LENGTH_SHORT).show()
} else {
startDownload()
}
}
private fun startDownload() {
if (this::downloadManager.isInitialized) {
val requestTaskStatus = downloadManager.getRequestStatus(getRequest.id)
when (requestTaskStatus) {
Result.STATUS.PAUSE -> {
Toast.makeText(
this,
"Press Resume Button to continue download process",
Toast.LENGTH_SHORT
).show()
return
}
Result.STATUS.PROCESS -> {
Toast.makeText(
this,
"First cancel the current download process",
Toast.LENGTH_SHORT
).show()
return
}
}
}
downloadManager = DownloadManager.Builder("downloadManager")
.build(this)
val fileName = downloadURL.substringAfterLast("/")
val downloadFilePath = this.cacheDir.path + File.separator + fileName
val currentDownloadURL = binding.urlTextInputEditText.text.toString()
getRequest = DownloadManager.newGetRequestBuilder()
.filePath(downloadFilePath)
.url(currentDownloadURL)
.speedLimit(downloadSpeedLimit)
.enableSlice(isEnableSlicedDownload)
.build()
val result = downloadManager.start(getRequest, fileRequestCallback)
if (result.code != Result.SUCCESS) {
Log.d(TAG, "An Error occurred when downloading")
}
}
private fun convertByteToMb(sizeInByte: Long): String? {
return if (sizeInByte < 0 || sizeInByte == 0L) {
null
} else {
val sizeInMb: Float = sizeInByte / (1024 * 1024).toFloat()
String.format("%.2f", sizeInMb)
}
}
private fun showCurrentDownloadSpeed(speedInByte: Long) {
val downloadSpeedText = if (speedInByte <= 0) {
"-"
} else {
val sizeInKb: Float = speedInByte / 1024.toFloat()
String.format("%.2f", sizeInKb) + "kB/s"
}
binding.currentSpeedTextView.text = downloadSpeedText
}
private fun calculateSpeedLimitAsByte(progressBarValue: Int): Int {
return when (progressBarValue) {
0 -> 512 * 1024
1 -> 1024 * 1024
2 -> 2 * 1024 * 1024
3 -> 4 * 1024 * 1024
4 -> 6 * 1024 * 1024
5 -> 8 * 1024 * 1024
6 -> 16 * 1024 * 1024
7 -> 0
else -> 0
}
}
private fun showDownloadSpeedLimit(progressValue: Int) {
val message = when (progressValue) {
0 -> "512 kB/s"
1 -> "1 mB/s"
2 -> "2 mB/s"
3 -> "4 mB/s"
4 -> "6 mB/s"
5 -> "8 mB/s"
6 -> "16 mB/s"
7 -> "Limitless"
else -> "Error"
}
binding.speedLimitTextView.text = message
}
private fun isDownloadManagerInitialized(): Boolean {
return if (this::downloadManager.isInitialized) {
true
} else {
Toast.makeText(this, "First start the download", Toast.LENGTH_SHORT).show()
false
}
}
private fun pasteClipboardData() {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = clipboardManager.primaryClip
val clipItem = clipData?.getItemAt(0)
val text = clipItem?.text.toString()
if (text == "null") {
Toast.makeText(this, "There is no text on clipboard", Toast.LENGTH_SHORT).show()
} else {
binding.urlTextInputEditText.setText(text)
}
}
private fun showRemainingTime(progress: Progress) {
val elapsedTime = System.nanoTime() - startTime
val allTimeForDownloading =
(elapsedTime * progress.totalSize / progress.finishedSize)
val remainingTime = allTimeForDownloading - elapsedTime
val hours = TimeUnit.NANOSECONDS.toHours(remainingTime)
val minutes = TimeUnit.NANOSECONDS.toMinutes(remainingTime) % 60
val seconds = TimeUnit.NANOSECONDS.toSeconds(remainingTime) % 60
val remainingTimeAsText = if (hours > 0) {
"${hours}h ${minutes}m ${seconds}s left"
} else {
if (minutes > 0) {
"${minutes}m ${seconds}s left"
} else {
"${seconds}s left"
}
}
binding.remainingTimeTextView.text = remainingTimeAsText
}
private fun showSnackBar(rootView: View, message: String) {
val snackBar = Snackbar.make(rootView, message, Snackbar.LENGTH_SHORT)
snackBar.show()
}
private fun clearAllViews() {
binding.percentProgressTextView.text = "0%"
binding.finishedSizeTextView.text = "0"
binding.totalSizeTextView.text = "0"
binding.currentSpeedTextView.text = "0 kB/s"
binding.downloadProgressProgressBar.progress = 0
binding.remainingTimeTextView.text = "0s left"
}
}
Tips & Tricks
According to the Wi-Fi status awareness capability of the Huawei Awareness Kit, you can pause or resume your download task. It will reduce the cost to the user and help to manage your download process properly.
Before starting the download task, you can check that you’re connected to the internet using the ConnectivityManager.
If the download file has the same name as an existing file, it will overwrite the existing file. Therefore, you should give different names for your files.
Even if you minimize the application, the download will continue in the background.
Conclusion
In this article, we have learned how to use Network Kit in your download tasks. And, we’ve developed the Download Manager app that provides many features. In addition to these features, you can also use Network Kit in your upload tasks. Please do not hesitate to ask your questions as a comment.
Thank you for your time and dedication. I hope it was helpful. See you in other articles.
References
Huawei Network Kit Official Documentation
Huawei Network Kit Official Codelab
Huawei Network Kit Official Github
Thanks for sharing.
{
"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 how to enter the data in Huawei CloudDB. Suppose, if user wants to insert, update, delete and query their data in safe mode, then the Huawei Cloud databases provides trusted platform for any business level. To do operations in clouddb, user must be authorized by Huawei Authentication Modes, such as Mobile Number, Email address, Huawei ID etc of Auth Service. Auth Service provides an SDK and backend services, supports multiple authentication modes, and provides a powerful management console, enabling you to easily develop and manage user authentication.
If you are new to this application, follow my previous articles:
Beginner: Integration of Huawei Account Kit of Obtaining Icon Resources feature in Attendance Tracker Android app (Kotlin) - Part 1
Beginner: Find Log in via SMS and Forgot password? Features using Huawei Account Kit, and Ads Kit in Attendance Tracker Android app (Kotlin) – Part 2
Beginner: Find the CRUD operations in Room Database in Attendance Tracker Android app (Kotlin) – Part 3
Beginner: Integration of Huawei Crash Service in Attendance Tracker Android app (Kotlin) – Part 4
Beginner: Enter the attendance timings using Room Database and Huawei Analytics Kit in Attendance Tracker Android app (Kotlin) – Part 5
Auth Service
By integrating the Auth Service SDK into your app, you can easily and quickly provide functions such as registration and sign-in for your users. You can choose to provide your users with one or more of the authentication modes.
Cloud DB
Huawei Cloud DB is a device-cloud synergy database product that enables seamless data synchronization between the device and cloud and between devices, and supports offline application operations, helping you quickly develop device-cloud and multi-device synergy applications.
AppGallery Connect
Cloud DB:
1. Choose My Projects > Project Settings > Build > Cloud DB, and click Enable now.
2. Click Add.
3. Enter Object Type Name, Fields and select Data Permissions.
4. Find the updated Fields and selected Data Permissions.
5. Click Export, select Java, android and Package Name, and click OK.
6. Now the ObjectTypeInfoHelper.java and StudentInfo.java model classes will be downloaded, and add it in your Android Project.
Auth Service:
1. Choose My Projects > Project Settings > Build > Auth Service, and enable HUAWEI ID.
2. Enter Client ID and Client Secret details from .json file, and click OK.
Requirements
1. Any operating system (MacOS, Linux and Windows).
2. Must have a Huawei phone with HMS 4.0.0.300 or later.
3. Must have a laptop or desktop with Android Studio, Jdk 1.8, SDK platform 26 and Gradle 4.6 and above installed.
4. Minimum API Level 24 is required.
5. Required EMUI 9.0.0 and later version devices.
How to integrate HMS Dependencies
1. First register as Huawei developer and complete identity verification in Huawei developers website, refer to register a Huawei ID.
2. Create a project in android studio, refer Creating an Android Studio Project.
3. Generate a SHA-256 certificate fingerprint.
4. To generate SHA-256 certificate fingerprint. On right-upper corner of android project click Gradle, choose Project Name > Tasks > android, and then click signingReport, as follows.
Note: Project Name depends on the user created name.
5. Create an App in AppGallery Connect.
6. Download the agconnect-services.json file from App information, copy and paste in android Project under app directory, as follows.
7. Enter SHA-256 certificate fingerprint and click Save button, as follows.
Note: Above steps from Step 1 to 7 is common for all Huawei Kits.
8. Click Manage APIs tab and enable Auth Service.
9. Add the below maven URL in build.gradle(Project) file under the repositories of buildscript, dependencies and allprojects, refer Add Configuration.
Java:
maven { url 'http://developer.huawei.com/repo/' }
classpath 'com.huawei.agconnect:agcp:1.6.0.300'
10. Add the below plugin and dependencies in build.gradle(Module) file.
Java:
apply plugin: id 'com.huawei.agconnect'
// Huawei AGC
implementation 'com.huawei.agconnect:agconnect-core:1.6.0.300'
// Huawei Account Kit
implementation 'com.huawei.hms:hwid:6.3.0.301'
// Cloud DB
implementation 'com.huawei.agconnect:agconnect-cloud-database:1.5.0.300'
// Auth Service
implementation "com.huawei.agconnect:agconnect-auth-huawei:1.6.0.300"
implementation 'com.huawei.agconnect:agconnect-auth:1.5.0.300'
11. Now Sync the gradle.
12. Add the required permission to the AndroidManifest.xml file.
Java:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Let us move to development
I have created a project on Android studio with empty activity let us start coding.
In the MainActivity.kt activity to find the business logic for Auth Service.
Java:
// Obtain the authorization for the HUAWEI ID
private fun findResources() {
mAuthParam = AccountAuthParamsHelper(AccountAuthParams.DEFAULT_AUTH_REQUEST_PARAM)
.setAccessToken()
.createParams()
mAuthManager = AccountAuthManager.getService([email protected], mAuthParam)
startActivityForResult(mAuthManager?.signInIntent, 1002)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 1002 ) {
// Auth Service, obtaining an access token after successful authorization
val authAccountTask = AccountAuthManager.parseAuthResultFromIntent(data)
if (authAccountTask.isSuccessful) {
val authAccount = authAccountTask.result
Log.i(TAG, "accessToken:" + authAccount.accessToken)
// Auth Service, to generate a credential using the obtained access token
val credential = HwIdAuthProvider.credentialWithToken(authAccount.accessToken)
AGConnectAuth.getInstance().signIn(credential).addOnSuccessListener { it ->
Toast.makeText(this, "Auth success", Toast.LENGTH_LONG).show()
val intent = Intent([email protected], Home::class.java)
startActivity(intent)
}.addOnFailureListener {
Toast.makeText(this, "Auth failed: ", Toast.LENGTH_LONG).show()
}
}
else {
Toast.makeText(this, "SignIn failed: " + (authAccountTask.exception as ApiException).statusCode, Toast.LENGTH_LONG).show()
}
}
}
In the CloudActivity.kt activity to find the business logic for button clicks.
Java:
class CloudActivity : AppCompatActivity() {
val TAG = "HomeScreen"
var mCloudDBZone: CloudDBZone? = null
var openCloudDbBtn: Button? = null
var insertDataBtn: Button? = null
var queryDataBtn: Button? = null
var registerUpdateListenerBtn: Button? = null
private var mSnapshotListeners = OnSnapshotListener<StudentInfo?> { cloudDBZoneSnapshot, e ->
if (e != null) {
Log.w("TAG", "onSnapshot: " + e.message)
[email protected]
}
val snapshotObjects = cloudDBZoneSnapshot.snapshotObjects
val studentInfos: MutableList<StudentInfo?> = ArrayList()
try {
if (snapshotObjects != null) {
while (snapshotObjects.hasNext()) {
val studentInfo: StudentInfo? = snapshotObjects.next() as StudentInfo?
studentInfos.add(studentInfo)
}
if (studentInfos.size > 0) {
runOnUiThread {
Toast.makeText(this,"You have entered some value that is matched to the checked condition", Toast.LENGTH_SHORT).show()
}
}
}
} catch (snapshotException: AGConnectCloudDBException) {
Log.w("TAG", "onSnapshot:(getObject) " + snapshotException.message)
} finally {
cloudDBZoneSnapshot.release()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cloud)
openCloudDbBtn = findViewById(R.id.btn_dbopen)
insertDataBtn = findViewById(R.id.btn_dbinsert)
queryDataBtn = findViewById(R.id.btn_dbgetall)
registerUpdateListenerBtn = findViewById(R.id.btn_dbreg)
openCloudDbBtn!!.setOnClickListener(View.OnClickListener { setAGC_DB() })
insertDataBtn!!.setOnClickListener(View.OnClickListener { insertDetails(mCloudDBZone!!) })
queryDataBtn!!.setOnClickListener(View.OnClickListener { queryAllStudents() })
registerUpdateListenerBtn!!.setOnClickListener(View.OnClickListener { startRegisterFragment() })
}
private fun setAGC_DB() {
// val user = AGConnectAuth.getInstance().currentUser
AGConnectCloudDB.initialize(applicationContext)
val mCloudDB = AGConnectCloudDB.getInstance()
try {
mCloudDB.createObjectType(ObjectTypeInfoHelper.getObjectTypeInfo())
} catch (e: AGConnectCloudDBException) {
e.printStackTrace()
}
val mConfig = CloudDBZoneConfig("Zone1",
CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE,
CloudDBZoneConfig.CloudDBZoneAccessProperty.CLOUDDBZONE_PUBLIC)
mConfig.persistenceEnabled = true
val openDBZoneTask = mCloudDB.openCloudDBZone2(mConfig, true)
openDBZoneTask.addOnSuccessListener { cloudDBZone ->
Toast.makeText(this,"Cloud DB successfully opened", Toast.LENGTH_SHORT).show()
if (mCloudDBZone == null) {
mCloudDBZone = cloudDBZone
}
}.addOnFailureListener { e -> e.printStackTrace() }
}
private fun queryAllStudents() {
if (mCloudDBZone == null) {
Log.w(TAG, "CloudDBZone is null, try re-open it")
return
}
val queryTask: Task<CloudDBZoneSnapshot<StudentInfo>> = mCloudDBZone!!.executeQuery(
CloudDBZoneQuery.where(StudentInfo::class.java),
CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY)
queryTask.addOnSuccessListener { snapshot -> processQueryResult(snapshot) }
.addOnFailureListener { e -> e.message
}
}
private fun processQueryResult(snapshot: CloudDBZoneSnapshot<StudentInfo>) {
val studentInfoCursor: CloudDBZoneObjectList<StudentInfo> = snapshot.snapshotObjects
val studentInfoList: MutableList<StudentInfo> = ArrayList<StudentInfo>()
try {
while (studentInfoCursor.hasNext()) {
val studentInfo: StudentInfo = studentInfoCursor.next()
studentInfoList.add(studentInfo)
}
val fm = supportFragmentManager
val customDialogFragment: RetrieveDialogFragment = RetrieveDialogFragment
.newInstance1("Database Details", studentInfoList)
customDialogFragment.show(fm, "RetrieveDialogFragment")
} catch (e: AGConnectCloudDBException) {
Log.w(TAG, "processQueryResult: " + e.message)
}
snapshot.release()
}
private fun insertDetails(mCloudDBZone: CloudDBZone) {
val fm = supportFragmentManager
val customDialogFragment: InsertDialogFragment = InsertDialogFragment
.newInstance2("Enter Details", mCloudDBZone)
customDialogFragment.show(fm, "InsertDialogFragment")
}
fun startRegisterFragment() {
val fm = supportFragmentManager
val customDialogFragment = RegisterUpdateListenerFragment
.newInstance("Enter Register Details", mCloudDBZone, this.mSnapshotListeners)
customDialogFragment!!.show(fm, "RegisterUpdateListenerFragment")
}
}
Create a InsertDialogFragment.kt fragment to insert the details into CloudDB.
Java:
class InsertDialogFragment : DialogFragment() {
var submitBtn: Button? = null
var nameField: EditText? = null
var ageField: EditText? = null
var idField: EditText? = null
var genderField: EditText? = null
override fun onCreate(savedInstanceState: Bundle?) {
setStyle(STYLE_NO_TITLE, android.R.style.Theme_Holo_Light_Dialog_NoActionBar_MinWidth)
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_insert_dialog, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
submitBtn = view.findViewById(R.id.btn_submit)
nameField = view.findViewById(R.id.edt_stu_name)
idField = view.findViewById(R.id.edt_stu_id)
ageField = view.findViewById(R.id.edt_stu_age)
genderField = view.findViewById(R.id.edt_stu_gender)
submitBtn!!.setOnClickListener(View.OnClickListener {
val studentInfo = StudentInfo()
studentInfo.name = nameField!!.text.toString()
studentInfo.age = Integer.valueOf(ageField!!.text.toString())
studentInfo.studentId = idField!!.text.toString()
studentInfo.gender = genderField!!.text.toString()
val upsertTask: Task<Int> = cloudDBZone!!.executeUpsert(studentInfo)
upsertTask.addOnSuccessListener {
Toast.makeText(activity,"Data inserted successfully", Toast.LENGTH_SHORT).show()
}.addOnFailureListener { e ->
e.printStackTrace()
Toast.makeText(activity, e.message, Toast.LENGTH_SHORT).show()
}
})
dialog!!.window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
}
companion object {
var cloudDBZone: CloudDBZone? = null
fun newInstance2(title: String?, cloudDBZone: CloudDBZone?): InsertDialogFragment {
val frag = InsertDialogFragment()
Companion.cloudDBZone = cloudDBZone
val args = Bundle()
args.putString("title", title)
frag.arguments = args
return frag
}
}
}
Create a RetrieveDialogFragment.kt fragment to find the recycler view logic.
Java:
class RetrieveDialogFragment : DialogFragment() {
var recyclerView: RecyclerView? = null
var linearLayoutManager: LinearLayoutManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
setStyle(STYLE_NO_TITLE, android.R.style.Theme_Holo_Light_Dialog_NoActionBar_MinWidth)
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view: View = inflater.inflate(R.layout.fragment_retrieve_dialog, container)
recyclerView = view.findViewById(R.id.stu_list)
recyclerView!!.setHasFixedSize(true)
linearLayoutManager = LinearLayoutManager(activity)
recyclerView!!.layoutManager = linearLayoutManager
recyclerView!!.itemAnimator = DefaultItemAnimator()
recyclerView!!.adapter = studentListAdapter
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = view.findViewById(R.id.stu_list)
dialog!!.window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
}
companion object {
var studentListAdapter: StudentAdapter? = null
var studentInfos: List<StudentInfo>? = null
fun newInstance1(title: String?, studentInfos: List<StudentInfo>?): RetrieveDialogFragment {
val frag = RetrieveDialogFragment()
Companion.studentInfos = studentInfos
studentListAdapter = StudentAdapter(studentInfos!!)
val args = Bundle()
args.putString("title", title)
frag.arguments = args
return frag
}
}
}
Create a StudentAdapter.kt adapter class to hold the list.
Java:
class StudentAdapter(localDataSet: List<StudentInfo>) : RecyclerView.Adapter<StudentAdapter.StuViewHolder>() {
private val localDataSet: List<StudentInfo>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StuViewHolder {
val view: View = LayoutInflater.from(parent.context)
.inflate(R.layout.student_list, parent, false)
return StuViewHolder(view)
}
override fun onBindViewHolder(holder: StuViewHolder, position: Int) {
holder.stuName.text = localDataSet[position].name
holder.stuId.text = localDataSet[position].studentId
holder.stuGender.text = localDataSet[position].gender
holder.stuAge.text = "Age : " + String.valueOf(localDataSet[position].age)
}
override fun getItemCount(): Int {
return localDataSet.size
}
class StuViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var stuName: TextView
var stuId: TextView
var stuAge: TextView
var stuGender: TextView
init {
stuName = itemView.findViewById(R.id.stu_name)
stuId = itemView.findViewById(R.id.stu_id)
stuAge = itemView.findViewById(R.id.stu_age)
stuGender = itemView.findViewById(R.id.stu_gen)
}
}
init {
this.localDataSet = localDataSet
}
}
Create a RegisterUpdateListenerFragment.kt fragment to register the fields.
Java:
class RegisterUpdateListenerFragment : DialogFragment() {
var mRegister: ListenerHandler? = null
var registerBtn: Button? = null
var activity: Activity? = null
var radioGroup: RadioGroup? = null
var nameEditText: EditText? = null
var ageEditText: EditText? = null
var genderEditText: EditText? = null
var stuIdEditText: EditText? = null
override fun onCreate(savedInstanceState: Bundle?) {
setStyle(STYLE_NO_TITLE, android.R.style.Theme_DeviceDefault_Light_NoActionBar_Fullscreen)
super.onCreate(savedInstanceState)
activity = getActivity()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_register_update_listener, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
radioGroup = view.findViewById(R.id.RGroup)
nameEditText = view.findViewById(R.id.name_register)
ageEditText = view.findViewById(R.id.age_register)
stuIdEditText = view.findViewById(R.id.id_register)
genderEditText = view.findViewById(R.id.gender_register)
registerBtn = view.findViewById(R.id.register_btn)
registerBtn!!.setOnClickListener(View.OnClickListener { addSubscription() })
radioGroup!!.setOnCheckedChangeListener(RadioGroup.OnCheckedChangeListener { group, checkedId ->
when (checkedId) {
R.id.name_radio -> {
nameEditText!!.visibility = View.VISIBLE
ageEditText!!.visibility = View.GONE
stuIdEditText!!.visibility = View.GONE
genderEditText!!.visibility = View.GONE
}
R.id.age_radio -> {
nameEditText!!.visibility = View.GONE
ageEditText!!.visibility = View.VISIBLE
stuIdEditText!!.visibility = View.GONE
genderEditText!!.visibility = View.GONE
}
R.id.id_radio -> {
nameEditText!!.visibility = View.GONE
ageEditText!!.visibility = View.GONE
stuIdEditText!!.visibility = View.VISIBLE
genderEditText!!.visibility = View.GONE
}
R.id.gender_radio -> {
nameEditText!!.visibility = View.GONE
ageEditText!!.visibility = View.GONE
stuIdEditText!!.visibility = View.GONE
genderEditText!!.visibility = View.VISIBLE
}
else -> {}
}
})
dialog!!.window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
}
fun addSubscription() {
if (cloudDBZone == null) {
Log.w("TAG", "CloudDBZone is null, try re-open it")
return
}
val checkedConditionName: String
val checkedConditionValue: String
var snapshotQuery: CloudDBZoneQuery<StudentInfo?> =
CloudDBZoneQuery.where(StudentInfo::class.java).equalTo("Age", 10)
when (radioGroup!!.checkedRadioButtonId) {
R.id.name_radio -> {
checkedConditionName = "StudentName"
checkedConditionValue = nameEditText!!.text.toString()
snapshotQuery = CloudDBZoneQuery.where(StudentInfo::class.java)
.equalTo(checkedConditionName, checkedConditionValue)
}
R.id.age_radio -> {
checkedConditionName = "Age"
checkedConditionValue = ageEditText!!.text.toString()
snapshotQuery = CloudDBZoneQuery.where(StudentInfo::class.java)
.equalTo(checkedConditionName, checkedConditionValue.toInt())
}
R.id.gender_radio -> {
checkedConditionName = "Gender"
checkedConditionValue = genderEditText!!.text.toString()
snapshotQuery = CloudDBZoneQuery.where(StudentInfo::class.java)
.equalTo(checkedConditionName, checkedConditionValue)
}
R.id.id_radio -> {
checkedConditionName = "StudentId"
checkedConditionValue = stuIdEditText!!.text.toString()
snapshotQuery = CloudDBZoneQuery.where(StudentInfo::class.java)
.equalTo(checkedConditionName, checkedConditionValue)
}
}
try {
mRegister = cloudDBZone!!.subscribeSnapshot(
snapshotQuery, CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_ONLY, mSnapshotListeners!!)
Toast.makeText(getActivity(),"Registered successfully", Toast.LENGTH_SHORT).show()
} catch (e: AGConnectCloudDBException) {
Log.w("TAG", "subscribeSnapshot: " + e.message)
}
}
companion object {
var mSnapshotListeners: OnSnapshotListener<StudentInfo?>? = null
var cloudDBZone: CloudDBZone? = null
fun newInstance(title: String?, cloudDBZone: CloudDBZone?, mSnapshotListeners: OnSnapshotListener<StudentInfo?>): RegisterUpdateListenerFragment? {
val frag = RegisterUpdateListenerFragment()
RegisterUpdateListenerFragment.cloudDBZone = cloudDBZone
RegisterUpdateListenerFragment.mSnapshotListeners = mSnapshotListeners
val args = Bundle()
args.putString("title", title)
frag.arguments = args
return frag
}
}
}
In the activity_cloud.xml we can create the UI screen.
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".CloudActivity">
<Button
android:id="@+id/btn_dbopen"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="40dp"
android:padding="5dp"
android:textColor="@color/white"
android:textSize="18sp"
android:textAllCaps="false"
android:text="Open CloudDB " />
<Button
android:id="@+id/btn_dbinsert"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="40dp"
android:padding="5dp"
android:textColor="@color/white"
android:textSize="18sp"
android:textAllCaps="false"
android:text="Insert" />
<Button
android:id="@+id/btn_dbgetall"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="40dp"
android:padding="5dp"
android:textColor="@color/white"
android:textSize="18sp"
android:textAllCaps="false"
android:text="Find List" />
<Button
android:id="@+id/btn_dbreg"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="40dp"
android:padding="5dp"
android:textColor="@color/white"
android:textSize="18sp"
android:textAllCaps="false"
android:text="Register " />
</LinearLayout>
In the fragment_insert_dialog.xml we can create the UI screen.
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/edt_stu_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter name: "
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/edt_stu_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter ID: "
app:layout_constraintTop_toBottomOf="@id/edt_stu_name" />
<EditText
android:id="@+id/edt_stu_age"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter Age: "
app:layout_constraintTop_toBottomOf="@id/edt_stu_id" />
<EditText
android:id="@+id/edt_stu_gender"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter Gender: "
app:layout_constraintTop_toBottomOf="@id/edt_stu_age" />
<Button
android:id="@+id/btn_submit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:textSize="18sp"
android:text="Submit"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/edt_stu_gender" />
</androidx.constraintlayout.widget.ConstraintLayout>
In the fragment_retrieve_dialog.xml we can create the UI screen for recycler view.
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/stu_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
In the student_list.xml we can create the UI screen for customized items.
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/stu_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="10dp"
android:textSize="18sp"
android:text="Name: "
android:textColor="@color/teal_700"
android:textStyle="bold" />
<TextView
android:id="@+id/stu_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/teal_700"
android:text="Student ID: " />
<TextView
android:id="@+id/stu_gen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/teal_700"
android:text="Gender: " />
<TextView
android:id="@+id/stu_age"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:textColor="@color/teal_700"
android:text="Age: " />
</LinearLayout>
</androidx.cardview.widget.CardView>
In the fragment_register_update_listener.xml we can create the UI screen for fields.
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RadioGroup
android:id="@+id/RGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="50dp"
app:layout_constraintBottom_toTopOf="@id/text_field"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<RadioButton
android:id="@+id/name_radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:text="Name " />
<RadioButton
android:id="@+id/id_radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:text="StuID " />
<RadioButton
android:id="@+id/age_radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:text="Age " />
<RadioButton
android:id="@+id/gender_radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:text="Gender " />
</RadioGroup>
<LinearLayout
android:id="@+id/text_field"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="visible"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
app:layout_constraintBottom_toTopOf="@id/register_btn"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/RGroup">
<EditText
android:id="@+id/name_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="Enter Name: "
android:visibility="gone" />
<EditText
android:id="@+id/id_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="Enter ID: "
android:visibility="gone" />
<EditText
android:id="@+id/age_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="Enter Age: "
android:visibility="gone" />
<EditText
android:id="@+id/gender_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="Enter Gender "
android:visibility="gone" />
</LinearLayout>
<Button
android:id="@+id/register_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Register"
android:textSize="18sp"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_field" />
</androidx.constraintlayout.widget.ConstraintLayout>
Demo
Tips and Tricks
1. Make sure you are already registered as Huawei developer.
2. Set minSDK version to 24 or later, otherwise you will get AndriodManifest merge issue.
3. Make sure you have added the agconnect-services.json file to app folder.
4. Make sure you have added SHA-256 fingerprint without fail.
5. Make sure all the dependencies are added properly.
Conclusion
In this article, we have learned to enter the data in Huawei CloudDB. User can insert, update, delete and query their data in safe mode using Huawei Cloud databases provides trusted platform for any business level. User must be authorized by Huawei Authentication Modes, such as Mobile Number, Email address, Huawei ID etc. of Auth Service.
I hope you have read this article. If you found it is helpful, please provide likes and comments.
Reference
CloudDB – Documentation
CloudDB – Training Video
Auth Service – Documentation
Auth Service – Training Video
the app can run in huawei nova5zd, OS is HarmonyOS 2.0.0, but can't run in pixel4xl, android 13, showing "no permission", the main code is
Java:
public class TestLocationActivity extends AppCompatActivity {
public static final int LOCATION_CODE = 301;
public static final String TAG = "TestLocationActivity:wp";
private LocationManager locationManager;
private String locationProvider = null;
com.adan.gpsdemo.databinding.ActivityTestBinding activityTestBinding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityTestBinding = ActivityTestBinding.inflate(getLayoutInflater());
setContentView(activityTestBinding.getRoot());
getLocation();
}
private void getLocation(){
//1.获取位置管理器
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
//2.获取位置提供器,GPS或是NetWork
List<String> providers = locationManager.getProviders(true);
if (providers.contains(LocationManager.GPS_PROVIDER)) {
//如果是GPS
locationProvider = LocationManager.GPS_PROVIDER;
Log.v(TAG, "定位方式GPS");
} else if (providers.contains(LocationManager.NETWORK_PROVIDER)) {
//如果是Network
locationProvider = LocationManager.NETWORK_PROVIDER;
Log.v(TAG, "定位方式Network");
}else {
Toast.makeText(this, "没有可用的位置提供器", Toast.LENGTH_SHORT).show();
return;
}
if (Build.VERSION_CODES.M <= Build.VERSION.SDK_INT) {
//获取权限(如果没有开启权限,会弹出对话框,询问是否开启权限)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
//请求权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION}, LOCATION_CODE);
} else {
//3.获取上次的位置,一般第一次运行,此值为null
Location location = locationManager.getLastKnownLocation(locationProvider);
if (location!=null){
// notice textview change
showGPSValue(location.getLongitude(),location.getLatitude());
Log.v(TAG, "获取上次的位置-经纬度:"+location.getLongitude()+" "+location.getLatitude());
getAddress(location);
}else{
//监视地理位置变化,第二个和第三个参数分别为更新的最短时间minTime和最短距离minDistace
locationManager.requestLocationUpdates(locationProvider, 3000, 1,locationListener);
}
}
} else {
Location location = locationManager.getLastKnownLocation(locationProvider);
if (location!=null){
showGPSValue(location.getLongitude(),location.getLatitude());
Log.v(TAG, "获取上次的位置-经纬度:"+location.getLongitude()+" "+location.getLatitude());
getAddress(location);
}else{
//监视地理位置变化,第二个和第三个参数分别为更新的最短时间minTime和最短距离minDistace
locationManager.requestLocationUpdates(locationProvider, 3000, 1,locationListener);
}
}
}
@SuppressLint("SetTextI18n")
private void showGPSValue(double longitude, double latitude){
new Thread(()->{
// 39.951111, 75.172778 change to 75°10'22''E,39°57'04''N
int hour = (int) longitude;
double minute = getDecimalValue(longitude) * 60;
double second = getDecimalValue(minute) * 60;
int hour_lat = (int) latitude;
double minute_lat = getDecimalValue(latitude) * 60;
double second_lat = getDecimalValue(minute_lat) * 60;
String strLong = hour + "°" + (int)minute + "'" + (int)second + "\"";
String strLat = hour_lat + "°" + (int)minute_lat + "'" + (int)second_lat + "\"";
Log.i(TAG,"longitude:" + strLong);
Log.i(TAG,"latitude:" + strLat);
activityTestBinding.tvGpsValue.setText(strLong + "E," + strLat + "N");
}).start();
}
/**
* get decimal of a value
* @param value double include int and decimal
* @return decimal of value
*/
private double getDecimalValue(double value){
long longPart = (long) value;
BigDecimal bigDecimal = new BigDecimal(Double.toString(value));
BigDecimal bigDecimalLongPart = new BigDecimal(Double.toString(longPart));
double dPoint = bigDecimal.subtract(bigDecimalLongPart).doubleValue();
Log.i(TAG,"DecimalValue:" + dPoint);
return dPoint;
}
public LocationListener locationListener;
{
locationListener = new LocationListener() {
// Provider的状态在可用、暂时不可用和无服务三个状态直接切换时触发此函数
@SuppressWarnings("deprecation")
@Contract(pure = true)
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
// Provider被enable时触发此函数,比如GPS被打开
@Override
public void onProviderEnabled(String provider) {
}
// Provider被disable时触发此函数,比如GPS被关闭
@Override
public void onProviderDisabled(String provider) {
}
//当坐标改变时触发此函数,如果Provider传进相同的坐标,它就不会被触发
@Override
public void onLocationChanged(Location location) {
if (location != null) {
//如果位置发生变化,重新显示地理位置经纬度
showGPSValue(location.getLongitude(),location.getLatitude());
Log.v(TAG, "监视地理位置变化-经纬度:" + location.getLongitude() + " " + location.getLatitude());
}
}
};
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == LOCATION_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "申请权限", Toast.LENGTH_LONG).show();
try {
List<String> providers = locationManager.getProviders(true);
if (providers.contains(LocationManager.NETWORK_PROVIDER)) {
//如果是Network
locationProvider = LocationManager.NETWORK_PROVIDER;
} else if (providers.contains(LocationManager.GPS_PROVIDER)) {
//如果是GPS
locationProvider = LocationManager.GPS_PROVIDER;
}
Location location = locationManager.getLastKnownLocation(locationProvider);
if (location != null) {
showGPSValue(location.getLongitude(),location.getLatitude());
Log.v("TAG", "获取上次的位置-经纬度:" + location.getLongitude() + " " + location.getLatitude());
} else {
// 监视地理位置变化,第二个和第三个参数分别为更新的最短时间minTime和最短距离minDistace
locationManager.requestLocationUpdates(locationProvider, 0, 0, locationListener);
}
} catch (SecurityException e) {
e.printStackTrace();
}
} else {
Toast.makeText(this, "缺少权限", Toast.LENGTH_LONG).show();
finish();
}
} else {
throw new IllegalStateException("Unexpected value: " + requestCode);
}
}
//获取地址信息:城市、街道等信息
private void getAddress(Location location) {
List<Address> result;
try {
if (location != null) {
Geocoder gc = new Geocoder(this, Locale.getDefault());
result = gc.getFromLocation(location.getLatitude(),
location.getLongitude(), 1);
new Thread(()->{
String addValue = result.toString();
activityTestBinding.tvAddressValue.setText(addValue);
}).start();
Log.v(TAG, "获取地址信息:"+ result);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
locationManager.removeUpdates(locationListener);
}
}
Click to expand...
Click to collapse
XML:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="ExtraText">
<!-- 粗略的位置权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- 精确的位置权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GPSDemo">
<activity
android:name=".TestFusedLocationProviderClientActivity"
android:exported="true">
<!-- <meta-data-->
<!-- android:name="android.app.lib_name"-->
<!-- android:value="" />-->
</activity>
<activity android:name=".MainActivity"></activity>
<activity android:name=".TestGPS2"></activity>
<activity android:name=".TestLocationActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".Test1" />
<activity android:name=".Test2" />
</application>
</manifest>
whloe code is
GitHub - yhm2046/GPSDemo: test gps
test gps. Contribute to yhm2046/GPSDemo development by creating an account on GitHub.
github.com
mm11751 said:
the app can run in huawei nova5zd, OS is HarmonyOS 2.0.0, but can't run in pixel4xl, android 13, showing "no permission", the main code is
XML:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="ExtraText">
<!-- 粗略的位置权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- 精确的位置权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GPSDemo">
<activity
android:name=".TestFusedLocationProviderClientActivity"
android:exported="true">
<!-- <meta-data-->
<!-- android:name="android.app.lib_name"-->
<!-- android:value="" />-->
</activity>
<activity android:name=".MainActivity"></activity>
<activity android:name=".TestGPS2"></activity>
<activity android:name=".TestLocationActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".Test1" />
<activity android:name=".Test2" />
</application>
</manifest>
whloe code is
GitHub - yhm2046/GPSDemo: test gps
test gps. Contribute to yhm2046/GPSDemo development by creating an account on GitHub.
github.com
Click to expand...
Click to collapse
Most likely because Android 13 includes behavior changes that may affect your app. The following behavior changes apply exclusively to apps that are targeting Android 13 or higher.
Privacy
Granular media permissions
Use of body sensors in the background requires new permission
Performance and battery
User experience
App color theme applied automatically to WebView content
Google Play services
Updated non-SDK restrictions
rodken said:
Most likely because Android 13 includes behavior changes that may affect your app. The following behavior changes apply exclusively to apps that are targeting Android 13 or higher.
Privacy
Granular media permissions
Use of body sensors in the background requires new permission
Performance and battery
User experience
App color theme applied automatically to WebView content
Google Play services
Updated non-SDK restrictions
Click to expand...
Click to collapse
I modify some code , but sitll don't work. Now how can I do ?
{
"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"
}
mm11751 said:
I modify some code , but sitll don't work. Now how can I do ?
Click to expand...
Click to collapse
Try API Level 33.
rodken said:
Try API Level 33.
Click to expand...
Click to collapse
I modified this code, still no work
mm11751 said:
I modified this code, still no work
Click to expand...
Click to collapse
Are you the developer of the app?
rodken said:
Are you the developer of the app?
Click to expand...
Click to collapse
yes, I wrote it
mm11751 said:
yes, I wrote it
Click to expand...
Click to collapse
You might want to redirect your question(s) to this forum to receive more than one insight on the issue.