diff --git a/Mobile/Downloader/Downloader.sln b/Mobile/Downloader/Downloader.sln new file mode 100644 index 000000000..ce50a4b91 --- /dev/null +++ b/Mobile/Downloader/Downloader.sln @@ -0,0 +1,22 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31005.135 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Downloader", "Downloader/Downloader.csproj", "{d42f4c90-c837-4704-8563-194d9392b9c7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {d42f4c90-c837-4704-8563-194d9392b9c7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {d42f4c90-c837-4704-8563-194d9392b9c7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {d42f4c90-c837-4704-8563-194d9392b9c7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {d42f4c90-c837-4704-8563-194d9392b9c7}.Release|Any CPU.Build.0 = Release|Any CPU + + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Mobile/Downloader/Downloader/Directory.Build.targets b/Mobile/Downloader/Downloader/Directory.Build.targets new file mode 100644 index 000000000..49b3ab641 --- /dev/null +++ b/Mobile/Downloader/Downloader/Directory.Build.targets @@ -0,0 +1,21 @@ + + + + + + + + $([System.IO.Path]::GetDirectoryName($(MSBuildProjectDirectory))) + + + + + + diff --git a/Mobile/Downloader/Downloader/DownloadInfo.cs b/Mobile/Downloader/Downloader/DownloadInfo.cs new file mode 100644 index 000000000..a7737c1a3 --- /dev/null +++ b/Mobile/Downloader/Downloader/DownloadInfo.cs @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.ComponentModel; + +namespace Downloader +{ + /// + /// An internal class to show download information list + /// + internal class DownloadInfo : INotifyPropertyChanged + { + private string name; + private string value; + + /// + /// Constructor + /// + /// field name + /// field value + public DownloadInfo(string name, string value) + { + this.Name = name; + this.Value = value; + } + + /// + /// The field name of download information list + /// + public string Name + { + get => name; + set + { + if (name != value) + { + name = value; + OnPropertyChanged(nameof(Name)); + } + } + } + + /// + /// The field value of download information list + /// + public string Value + { + get => this.value; + set + { + if (this.value != value) + { + this.value = value; + OnPropertyChanged(nameof(Value)); + } + } + } + + // INotifyPropertyChanged implementation + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/Mobile/Downloader/Downloader/DownloadInfoPage.cs b/Mobile/Downloader/Downloader/DownloadInfoPage.cs new file mode 100644 index 000000000..da8cde076 --- /dev/null +++ b/Mobile/Downloader/Downloader/DownloadInfoPage.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using Tizen.NUI; +using Tizen.NUI.Components; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; + +namespace Downloader +{ + /// + /// A public class to show download information page in NUI. + /// + public class DownloadInfoPage : ContentPage + { + private View downloadInfoLabel; + private CollectionView downloadInfoListView; + private List downloadInfoList = new List(); + private bool isUpdatedInfoList; + private Button goToMainPageButton; + private IDownload downloadService; + + /// + /// Default constructor (creates a new download service instance) + /// + public DownloadInfoPage() : this(new DownloadImplementation()) + { + } + + /// + /// Constructor that accepts a shared download service instance. + /// + /// The IDownload service instance to use. + public DownloadInfoPage(IDownload sharedDownloadService) + { + downloadService = sharedDownloadService; + InitializeComponent(); + } + + /// + /// Initialize page events. + /// + private void InitializeEvents() + { + // Update a download information list when this page is appearing + this.Appearing += (object sender, PageAppearingEventArgs e) => + { + UpdateDownloadInfoList(); + }; + } + + /// + /// Initialize components of page. + /// + private void InitializeComponent() + { + AppBar = new AppBar + { + Title = "Download Info", + BackgroundColor = Resources.PrimaryColor + }; + + // Create and add the navigation button to the AppBar using TextLabel instead of Button + var mainButtonContainer = new View() + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Horizontal, + LinearAlignment = LinearLayout.Alignment.Center, + CellPadding = Resources.ZeroSpacing + }, + WidthSpecification = LayoutParamPolicies.WrapContent, + HeightSpecification = LayoutParamPolicies.MatchParent, + Padding = Resources.AppBarButtonPadding, + BackgroundColor = Resources.TransparentColor + }; + + var mainButtonText = new TextLabel + { + Text = "Main", + TextColor = Resources.WhiteColor, + PointSize = Resources.TextSizeMedium, + WidthSpecification = LayoutParamPolicies.WrapContent, + HeightSpecification = LayoutParamPolicies.WrapContent, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + + mainButtonContainer.Add(mainButtonText); + mainButtonContainer.TouchEvent += (s, e) => + { + if (e.Touch.GetState(0) == PointStateType.Up) + { + OnGoToMainPageButtonClicked(s, null); + } + return true; + }; + + goToMainPageButton = null; + AppBar.Actions = new View[] { mainButtonContainer }; + + downloadInfoLabel = CreateDownloadInfoLabel(); + downloadInfoListView = CreateCollectionView(); + + var mainLayout = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = Resources.LayoutSpacingMedium + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.MatchParent, + BackgroundColor = Resources.BackgroundColor, + Padding = Resources.PagePadding + }; + + mainLayout.Add(downloadInfoLabel); + mainLayout.Add(downloadInfoListView); + + Content = mainLayout; + + isUpdatedInfoList = false; + + // Initialize events after content is set + InitializeEvents(); + AddEvent(); // Add button events + } + + + /// + /// Register event handlers for buttons. + /// + private void AddEvent() + { + // goToMainPageButton.Clicked event is already subscribed in InitializeComponent + } + + /// + /// Event handler for the "Go to Download Main Page" button click. + /// Navigates back to the DownloadMainPage. + /// + /// Event sender + /// Event arguments + private void OnGoToMainPageButtonClicked(object sender, ClickedEventArgs e) + { + var navigator = this.Navigator; + if (navigator != null && navigator.PageCount > 1) + { + navigator.Pop(); + } + } + + /// + /// Update a download information list. + /// + private void UpdateDownloadInfoList() + { + // Update a download information list when download is completed + if (!isUpdatedInfoList && downloadService.GetDownloadState() == (int)Downloader.State.Completed) + { + // Clear existing list + downloadInfoList.Clear(); + + // Add downloaded URL to list + downloadInfoList.Add(new DownloadInfo("URL", downloadService.GetUrl())); + // Add downloaded content name to list + downloadInfoList.Add(new DownloadInfo("Content Name", downloadService.GetContentName())); + // Add downloaded content size to list + downloadInfoList.Add(new DownloadInfo("Content Size", downloadService.GetContentSize().ToString())); + // Add downloaded MIME type to list + downloadInfoList.Add(new DownloadInfo("MIME Type", downloadService.GetMimeType())); + // Add downloaded path to list + downloadInfoList.Add(new DownloadInfo("Download Path", downloadService.GetDownloadedPath())); + + // Update the CollectionView + downloadInfoListView.ItemsSource = downloadInfoList; + + isUpdatedInfoList = true; + } + } + + /// + /// Create a new Label container. + /// + /// View containing styled label + private View CreateDownloadInfoLabel() + { + var labelContainer = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = Resources.LayoutSpacingSmall + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + BackgroundColor = Resources.SurfaceColor, + CornerRadius = Resources.CornerRadiusMedium, + Padding = Resources.CardPadding + }; + + var label = new TextLabel + { + Text = "Download Information", + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + PointSize = Resources.TextSizeLarge, + TextColor = Resources.TextColor, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + + labelContainer.Add(label); + return labelContainer; + } + + /// + /// Create a new CollectionView + /// This CollectionView shows download information list. + /// + /// CollectionView + private CollectionView CreateCollectionView() + { + var collectionView = new CollectionView + { + ItemsSource = downloadInfoList, + ScrollingDirection = ScrollableBase.Direction.Vertical, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.MatchParent, + ItemsLayouter = new LinearLayouter() + }; + + collectionView.ItemTemplate = new DataTemplate(() => + { + var item = new DefaultLinearItem + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + BackgroundColor = Resources.SurfaceColor, + CornerRadius = Resources.CornerRadiusSmall, + Padding = Resources.CardPadding + }; + + // Create a horizontal layout for name and value + var itemLayout = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Horizontal, + CellPadding = Resources.ItemLayoutSpacing + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent + }; + + // Name label + var nameLabel = new TextLabel + { + WidthSpecification = Resources.NameLabelWidth, + HeightSpecification = LayoutParamPolicies.WrapContent, + PointSize = Resources.TextSizeMedium, + TextColor = Resources.TextColor + }; + nameLabel.SetBinding(TextLabel.TextProperty, "Name"); + + // Value label + var valueLabel = new TextLabel + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + PointSize = Resources.TextSizeMedium, + TextColor = Resources.TextColorSecondary, + HorizontalAlignment = HorizontalAlignment.Begin + }; + valueLabel.SetBinding(TextLabel.TextProperty, "Value"); + + itemLayout.Add(nameLabel); + itemLayout.Add(valueLabel); + + item.Add(itemLayout); + + return item; + }); + + return collectionView; + } + } +} diff --git a/Mobile/Downloader/Downloader/DownloadMainPage.cs b/Mobile/Downloader/Downloader/DownloadMainPage.cs new file mode 100644 index 000000000..fdf855878 --- /dev/null +++ b/Mobile/Downloader/Downloader/DownloadMainPage.cs @@ -0,0 +1,385 @@ +using System; +using System.Collections.Generic; // Added for IEnumerable<>, List<> +using Tizen.NUI; +using Tizen.NUI.Components; +using Tizen.NUI.BaseComponents; + +namespace Downloader +{ + /// + /// A class for download main page in NUI. + /// + public class DownloadMainPage : ContentPage + { + private TextField urlTextField; + private Button downloadButton; + private Button goToInfoPageButton; + private Progress progressBar; + private TextLabel progressLabel; + private string downloadUrl; + private IDownload downloadService; + + /// + /// Constructor. + /// + public DownloadMainPage() + { + // Initialize the download service + downloadService = new DownloadImplementation(); + InitializeComponent(); + } + + /// + /// Initialize main page. + /// Add components and events. + /// + private void InitializeComponent() + { + AppBar = new AppBar + { + Title = "Download", + BackgroundColor = Resources.PrimaryColor + }; + + // Create and add the navigation button to the AppBar using TextLabel instead of Button + var infoButtonContainer = new View() + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Horizontal, + LinearAlignment = LinearLayout.Alignment.Center, + CellPadding = Resources.ZeroSpacing + }, + WidthSpecification = LayoutParamPolicies.WrapContent, + HeightSpecification = LayoutParamPolicies.MatchParent, + Padding = Resources.AppBarButtonPadding, + BackgroundColor = Resources.TransparentColor + }; + + var infoButtonText = new TextLabel + { + Text = "Info", + TextColor = Resources.WhiteColor, + PointSize = Resources.TextSizeMedium, + WidthSpecification = LayoutParamPolicies.WrapContent, + HeightSpecification = LayoutParamPolicies.WrapContent, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + + infoButtonContainer.Add(infoButtonText); + infoButtonContainer.TouchEvent += (s, e) => + { + if (e.Touch.GetState(0) == PointStateType.Up) + { + OnGoToInfoPageButtonClicked(s, null); + } + return true; + }; + + goToInfoPageButton = null; + AppBar.Actions = new View[] { infoButtonContainer }; + + var mainLayout = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = Resources.LayoutSpacingMedium + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.MatchParent, + BackgroundColor = Resources.BackgroundColor, + Padding = Resources.FormPadding + }; + + // Create URL input section + var urlSection = CreateUrlSection(); + mainLayout.Add(urlSection); + + // Create download button + downloadButton = CreateDownloadButton(); + mainLayout.Add(downloadButton); + + // Create progress section + var progressSection = CreateProgressSection(); + mainLayout.Add(progressSection); + + Content = mainLayout; + + AddEvent(); + } + + /// + /// Create URL input section. + /// + /// View containing URL input + private View CreateUrlSection() + { + var section = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = Resources.LayoutSpacingSmall + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + BackgroundColor = Resources.SurfaceColor, + CornerRadius = Resources.CornerRadiusMedium, + Padding = Resources.CardPadding + }; + + // URL label + var urlLabel = new TextLabel + { + Text = "Download URL", + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + PointSize = Resources.TextSizeMedium, + TextColor = Resources.TextColor + }; + section.Add(urlLabel); + + // Add spacing + var spacer1 = new View + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = Resources.SpacingSmall + }; + section.Add(spacer1); + + // URL text field + urlTextField = new TextField + { + PlaceholderText = "Enter URL to download...", + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = Resources.TextFieldHeight, + MaxLength = int.MaxValue, + BackgroundColor = Resources.SurfaceColor, + TextColor = Resources.TextColor, + PlaceholderTextColor = Resources.TextColorSecondary, + PointSize = Resources.TextSizeMedium, + CornerRadius = Resources.CornerRadiusSmall, + Padding = Resources.TextFieldPadding + }; + section.Add(urlTextField); + + return section; + } + + /// + /// Create a new Button to start download. + /// + /// Button + private Button CreateDownloadButton() + { + var button = new Button(Resources.PrimaryButtonStyle) + { + Text = "Start Download", + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = Resources.ButtonHeight, + BackgroundColor = Resources.PrimaryColor, + TextColor = Resources.WhiteColor, + PointSize = Resources.TextSizeMedium, + CornerRadius = Resources.CornerRadiusMedium + }; + + return button; + } + + + /// + /// Create progress section with progress bar and label. + /// + /// View containing progress elements + private View CreateProgressSection() + { + var section = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = Resources.LayoutSpacingSmall + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + BackgroundColor = Resources.SurfaceColor, + CornerRadius = Resources.CornerRadiusMedium, + Padding = Resources.CardPadding + }; + + // Progress label + var progressTitleLabel = new TextLabel + { + Text = "Download Progress", + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + PointSize = Resources.TextSizeMedium, + TextColor = Resources.TextColor + }; + section.Add(progressTitleLabel); + + // Add spacing + var spacer2 = new View + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = Resources.SpacingSmall + }; + section.Add(spacer2); + + // Progress bar + progressBar = new Progress + { + CurrentValue = 0.0f, + MaxValue = 100.0f, + MinValue = 0.0f, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = Resources.ProgressBarHeight, + TrackColor = Resources.TrackColor, + ProgressColor = Resources.PrimaryColor, + CornerRadius = Resources.CornerRadiusSmall + }; + section.Add(progressBar); + + // Add spacing + var spacer3 = new View + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = Resources.SpacingSmall + }; + section.Add(spacer3); + + // Progress label + progressLabel = new TextLabel + { + Text = "0 bytes / 0 bytes", + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + PointSize = Resources.TextSizeSmall, + TextColor = Resources.TextColorSecondary + }; + section.Add(progressLabel); + + return section; + } + + /// + /// Register event handlers for entry, button and state callback. + /// + private void AddEvent() + { + downloadButton.Clicked += OnButtonClicked; + // goToInfoPageButton.Clicked event is already subscribed in InitializeComponent + downloadService.DownloadStateChanged += OnStateChanged; + downloadService.DownloadProgress += OnProgressbarChanged; + } + + /// + /// Event handler when download state is changed. + /// + /// Event sender + /// Event arguments including download state + private void OnStateChanged(object sender, DownloadStateChangedEventArgs e) + { + if (string.IsNullOrEmpty(e.stateMsg)) + return; + + downloadService.DownloadLog("State: " + e.stateMsg); + + // Update UI on main thread + NUIApplication.Post(() => + { + if (e.stateMsg == "Failed") + { + downloadButton.Text = e.stateMsg + "! Please start download again."; + // If download is failed, dispose a request + downloadService.Dispose(); + // Enable a download button + downloadButton.Sensitive = true; + } + else if (e.stateMsg != downloadButton.Text) + { + downloadButton.Text = e.stateMsg; + } + }); + } + + /// + /// Event handler when data is received. + /// + /// Event sender + /// Event arguments including received data size + private void OnProgressbarChanged(object sender, DownloadProgressEventArgs e) + { + if (e.ReceivedSize <= 0) + return; + + ulong contentSize = downloadService.GetContentSize(); + + // Update UI on main thread + NUIApplication.Post(() => + { + if (contentSize > 0) + { + progressBar.CurrentValue = (float)((double)e.ReceivedSize / contentSize * 100.0); + } + progressLabel.Text = e.ReceivedSize + " bytes / " + contentSize + " bytes"; + }); + } + + /// + /// Event handler when button is clicked. + /// Once a button is clicked and download is started, a button is disabled. + /// + /// Event sender + /// Event arguments + private void OnButtonClicked(object sender, ClickedEventArgs e) + { + downloadUrl = urlTextField.Text; + + downloadService.DownloadLog("Start Download: " + downloadUrl); + try + { + // Start to download content + downloadService.StartDownload(downloadUrl); + // Disable a button to avoid duplicated request. + downloadButton.Sensitive = false; + } + catch (Exception ex) + { + downloadService.DownloadLog("Request.Start() is failed: " + ex.Message); + // In case download is failed, enable a button. + downloadButton.Sensitive = true; + } + } + + /// + /// Event handler for the "Go to Download Info Page" button click. + /// Navigates to the DownloadInfoPage. + /// + /// Event sender + /// Event arguments + private void OnGoToInfoPageButtonClicked(object sender, ClickedEventArgs e) + { + this.Navigator?.Push(new DownloadInfoPage(this.downloadService)); + } + + /// + /// Clean up resources when page is disposed. + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + // Unsubscribe from events + if (downloadService != null) + { + downloadService.DownloadStateChanged -= OnStateChanged; + downloadService.DownloadProgress -= OnProgressbarChanged; + } + } + base.Dispose(disposing); + } + } +} diff --git a/Mobile/Downloader/Downloader/Downloader.cs b/Mobile/Downloader/Downloader/Downloader.cs new file mode 100644 index 000000000..ce0a0aa71 --- /dev/null +++ b/Mobile/Downloader/Downloader/Downloader.cs @@ -0,0 +1,37 @@ +using System; +using Tizen.NUI; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; + +namespace Downloader +{ + class Program : NUIApplication + { + private Window window; + private Navigator navigator; + + protected override void OnCreate() + { + base.OnCreate(); + Initialize(); + } + + void Initialize() + { + window = NUIApplication.GetDefaultWindow(); + navigator = window.GetDefaultNavigator(); + + var downloadMainPage = new DownloadMainPage(); + var downloadInfoPage = new DownloadInfoPage(); + + navigator.Push(downloadMainPage); + } + + static void Main(string[] args) + { + NUIApplication.IsUsingXaml = false; + var app = new Program(); + app.Run(args); + } + } +} diff --git a/Mobile/Downloader/Downloader/Downloader.csproj b/Mobile/Downloader/Downloader/Downloader.csproj new file mode 100644 index 000000000..64b007b4e --- /dev/null +++ b/Mobile/Downloader/Downloader/Downloader.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0-tizen9.0 + + + + portable + + + None + + + + + + + + diff --git a/Mobile/Downloader/Downloader/DownlodImplementation.cs b/Mobile/Downloader/Downloader/DownlodImplementation.cs new file mode 100644 index 000000000..189ca3328 --- /dev/null +++ b/Mobile/Downloader/Downloader/DownlodImplementation.cs @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using Tizen; +using Tizen.Content.Download; + +namespace Downloader +{ + /// + /// Implementation class of IDownload interface + /// + public class DownloadImplementation : IDownload + { + public event EventHandler DownloadStateChanged; + public event EventHandler DownloadProgress; + + private Request req; + + // Flag to check download is started or not + private bool isStarted = false; + + /// + /// Register event handler and call Request.Start() to start download + /// + /// The URL to download + public void StartDownload(string url) + { + // Create new Request with URL + req = new Request(url); + // Register state changed event handler + req.StateChanged += StateChanged; + // Register progress changed event handler + req.ProgressChanged += ProgressChanged; + isStarted = true; + // Start download content + req.Start(); + } + + /// + /// Get the URL to download + /// + /// The URL + public string GetUrl() + { + if (!isStarted) + { + return ""; + } + + return req.Url; + } + + /// + /// Get the content name of the downloaded file + /// + /// The content name + public string GetContentName() + { + if (!isStarted) + { + return ""; + } + + return req.ContentName; + } + + /// + /// Get the content size of the downloaded file + /// + /// The content size + public ulong GetContentSize() + { + if (!isStarted) + { + return 0; + } + + return req.ContentSize; + } + + /// + /// Get the downloaded path + /// + /// The downloaded path + public string GetDownloadedPath() + { + if (!isStarted) + { + return ""; + } + + return req.DownloadedPath; + } + + /// + /// Get the MIME type of the downloaded content + /// + /// The MIME type + public string GetMimeType() + { + if (!isStarted) + { + return ""; + } + + return req.MimeType; + } + + /// + /// Get the download state + /// + /// The download state + public int GetDownloadState() + { + if (!isStarted) + { + return 0; + } + + return (int)req.State; + } + + /// + /// Dispose the request + /// + public void Dispose() + { + if (isStarted) + { + req.Dispose(); + isStarted = false; + } + } + + /// + /// Show log message + /// + /// Log message + public void DownloadLog(String message) + { + // Show received message as DLOG + Log.Info("DOWNLOADER", message); + } + + /// + /// Event handler for download state. + /// + /// Event sender + /// State changed event arguments + private void StateChanged(object sender, StateChangedEventArgs e) + { + String stateMsg = ""; + + switch (e.State) + { + /// Download is ready. + case DownloadState.Ready: + stateMsg = "Ready"; + break; + /// Content is downloading. + case DownloadState.Downloading: + stateMsg = "Downloading"; + break; + /// Download is completed. + case DownloadState.Completed: + stateMsg = "Completed"; + break; + /// Download is failed. + case DownloadState.Failed: + stateMsg = "Failed"; + break; + /// Download is Paused. + case DownloadState.Paused: + stateMsg = "Paused"; + break; + /// Download is Queued. + case DownloadState.Queued: + stateMsg = "Queued"; + break; + /// Download is Canceled. + case DownloadState.Canceled: + stateMsg = "Canceled"; + break; + default: + stateMsg = ""; + break; + } + + // Send current state to event handler + DownloadStateChanged?.Invoke(sender, new DownloadStateChangedEventArgs(stateMsg)); + } + + /// + /// Event handler for download progress + /// + /// Event sender + /// Progress changed event arguments + private void ProgressChanged(object sender, ProgressChangedEventArgs e) + { + // If received data is exist, send data size to event handler + if (e.ReceivedDataSize > 0) + { + DownloadProgress?.Invoke(sender, new DownloadProgressEventArgs(e.ReceivedDataSize)); + } + } + } +} diff --git a/Mobile/Downloader/Downloader/IDownload.cs b/Mobile/Downloader/Downloader/IDownload.cs new file mode 100644 index 000000000..da69e4c2e --- /dev/null +++ b/Mobile/Downloader/Downloader/IDownload.cs @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.ComponentModel; + +namespace Downloader +{ + /// + /// Enumeration for the download states. + /// + public enum State + { + /// + /// None. + /// + None = 0, + /// + /// Ready to download. + /// + Ready, + /// + /// Queued to start downloading. + /// + Queued, + /// + /// Currently downloading. + /// + Downloading, + /// + /// The download is paused and can be resumed. + /// + Paused, + /// + /// The download is completed. + /// + Completed, + /// + /// The download failed. + /// + Failed, + /// + /// A user cancels the download request. + /// + Canceled + } + + /// + /// Interface to call Tizen.Content.Download + /// + public interface IDownload + { + event EventHandler DownloadStateChanged; + event EventHandler DownloadProgress; + + string GetContentName(); + ulong GetContentSize(); + string GetDownloadedPath(); + string GetMimeType(); + int GetDownloadState(); + string GetUrl(); + + void StartDownload(String url); + void Dispose(); + void DownloadLog(String msg); + } + + /// + /// An extended EventArgs class for download state + /// + public class DownloadStateChangedEventArgs : EventArgs + { + public String stateMsg = ""; + + public DownloadStateChangedEventArgs(String msg) + { + stateMsg = msg; + } + } + + /// + /// An extended EventArgs class for download progress + /// + public class DownloadProgressEventArgs : EventArgs + { + public ulong ReceivedSize = 0; + public DownloadProgressEventArgs(ulong size) + { + ReceivedSize = size; + } + } +} diff --git a/Mobile/Downloader/Downloader/Resources.cs b/Mobile/Downloader/Downloader/Resources.cs new file mode 100644 index 000000000..e055ec664 --- /dev/null +++ b/Mobile/Downloader/Downloader/Resources.cs @@ -0,0 +1,98 @@ +using System; +using Tizen.NUI; +using Tizen.NUI.Components; + +namespace Downloader +{ + /// + /// Static class to hold common resources, styles, and constants for the application + /// + internal static class Resources + { + // Common Colors + public static readonly Color PrimaryColor = new Color(0.1294f, 0.5882f, 0.9529f, 1.0f); // #2196F3 + public static readonly Color PrimaryDarkColor = new Color(0.094f, 0.4627f, 0.8235f, 1.0f); // #1976D2 + public static readonly Color PrimaryLightColor = new Color(0.2f, 0.7f, 1.0f, 1.0f); // #33B2FF + public static readonly Color AccentColor = new Color(0.0f, 0.7373f, 0.8314f, 1.0f); // #00BCE5 + public static readonly Color BackgroundColor = new Color(0.98f, 0.98f, 0.98f, 1.0f); // #FAFAFA + public static readonly Color SurfaceColor = Color.White; + public static readonly Color CardColor = Color.White; + public static readonly Color TextColor = new Color(0.13f, 0.13f, 0.13f, 1.0f); // #212121 + public static readonly Color TextColorSecondary = new Color(0.6f, 0.6f, 0.6f, 1.0f); // #999999 + public static readonly Color TextColorLight = new Color(0.87f, 0.87f, 0.87f, 1.0f); // #DEDEDE + public static readonly Color DisabledColor = new Color(0.7f, 0.7f, 0.7f, 1.0f); + public static readonly Color SuccessColor = new Color(0.298f, 0.686f, 0.314f, 1.0f); // #4CAF50 + public static readonly Color ErrorColor = new Color(0.957f, 0.263f, 0.212f, 1.0f); // #F44336 + public static readonly Color WarningColor = new Color(1.0f, 0.6f, 0.0f, 1.0f); // #FF9800 + public static readonly Color BorderColor = new Color(0.88f, 0.88f, 0.88f, 1.0f); // #E0E0E0 + public static readonly Color ShadowColor = new Color(0.0f, 0.0f, 0.0f, 0.1f); // Subtle shadow + public static readonly Color WhiteColor = Color.White; + public static readonly Color TrackColor = new Color(0.9f, 0.9f, 0.9f, 1.0f); + public static readonly Color TransparentColor = Color.Transparent; + + // Common Sizes + public const float TextSizeTitle = 28.0f; + public const float TextSizeLarge = 24.0f; + public const float TextSizeMedium = 20.0f; + public const float TextSizeSmall = 16.0f; + public const float TextSizeExtraSmall = 14.0f; + + // Common Spacing + public const int SpacingExtraSmall = 4; + public const int SpacingSmall = 8; + public const int SpacingMedium = 16; + public const int SpacingLarge = 24; + public const int SpacingExtraLarge = 32; + + // Component Heights + public const int ButtonHeight = 56; + public const int TextFieldHeight = 56; + public const int ProgressBarHeight = 8; + public const int TableHeight = 200; + + // Component Widths + public const int NameLabelWidth = 200; + + // Layout Spacing for specific components + public static readonly Size2D ItemLayoutSpacing = new Size2D(16, 0); + public static readonly Size2D ZeroSpacing = new Size2D(0, 0); + + // Border Radius + public static readonly Vector4 CornerRadiusSmall = new Vector4(4, 4, 4, 4); + public static readonly Vector4 CornerRadiusMedium = new Vector4(8, 8, 8, 8); + public static readonly Vector4 CornerRadiusLarge = new Vector4(12, 12, 12, 12); + + // Button Styles + public static readonly ButtonStyle PrimaryButtonStyle = new ButtonStyle + { + CornerRadius = CornerRadiusMedium, + Padding = new Extents(24, 24, 16, 16) + }; + + public static readonly ButtonStyle SecondaryButtonStyle = new ButtonStyle + { + CornerRadius = CornerRadiusMedium, + Padding = new Extents(24, 24, 16, 16) + }; + + public static readonly ButtonStyle AppBarButtonStyle = new ButtonStyle + { + CornerRadius = CornerRadiusSmall, + Padding = new Extents(16, 16, 8, 8) + }; + + // Layout Spacing + public static readonly Size2D LayoutSpacingExtraSmall = new Size2D(0, SpacingExtraSmall); + public static readonly Size2D LayoutSpacingSmall = new Size2D(0, SpacingSmall); + public static readonly Size2D LayoutSpacingMedium = new Size2D(0, SpacingMedium); + public static readonly Size2D LayoutSpacingLarge = new Size2D(0, SpacingLarge); + + // Padding + public static readonly Extents PagePadding = new Extents(SpacingLarge, SpacingLarge, SpacingLarge, SpacingLarge); + public static readonly Extents FormPadding = new Extents(SpacingLarge, SpacingLarge, SpacingMedium, SpacingMedium); + public static readonly Extents CardPadding = new Extents(SpacingMedium, SpacingMedium, SpacingMedium, SpacingMedium); + public static readonly Extents SectionPadding = new Extents(SpacingSmall, SpacingSmall, SpacingSmall, SpacingSmall); + public static readonly Extents TextFieldPadding = new Extents(16, 16, 12, 12); + public static readonly Extents AppBarButtonPadding = new Extents(40, 40, 8, 8); + } +} diff --git a/Mobile/Downloader/Downloader/shared/res/Downloader.png b/Mobile/Downloader/Downloader/shared/res/Downloader.png new file mode 100644 index 000000000..9f3cb9860 Binary files /dev/null and b/Mobile/Downloader/Downloader/shared/res/Downloader.png differ diff --git a/Mobile/Downloader/Downloader/tizen-manifest.xml b/Mobile/Downloader/Downloader/tizen-manifest.xml new file mode 100644 index 000000000..f7de68944 --- /dev/null +++ b/Mobile/Downloader/Downloader/tizen-manifest.xml @@ -0,0 +1,19 @@ + + + + + http://tizen.org/privilege/download + + + + Downloader.png + + + diff --git a/Mobile/Downloader/Downloader/tizen_dotnet_project.yaml b/Mobile/Downloader/Downloader/tizen_dotnet_project.yaml new file mode 100644 index 000000000..a64777207 --- /dev/null +++ b/Mobile/Downloader/Downloader/tizen_dotnet_project.yaml @@ -0,0 +1,24 @@ +# csproj file path +csproj_file: Downloader.csproj + +# Default profile, Tizen API version +profile: tizen +api_version: "9.0" + +# Build type [Debug/ Release/ Test] +build_type: Debug + +# Signing profile to be used for Tizen package signing +# If value is empty: "", active signing profile will be used +# Else If value is ".", default signing profile will be used +signing_profile: . + +# files monitored for dirty/modified status +files: + - Downloader.csproj + - Downloader.cs + - tizen-manifest.xml + - shared/res/Downloader.png + +# project dependencies +deps: [] diff --git a/Mobile/Downloader/README.md b/Mobile/Downloader/README.md new file mode 100644 index 000000000..2d5830c18 --- /dev/null +++ b/Mobile/Downloader/README.md @@ -0,0 +1,17 @@ +# Downloader +The Downloader application demonstrates how user can download contents and get the download information. + +![MainPage](./Screenshots/Tizen/DownloadMainPage.png) +![DownloadInfoPage](./Screenshots/Tizen/DownloadInfoPage.png) + + +### Verified Version +* Tizen.NET : 6.0.428 +* Tizen.NET.SDK : 10.0.111 + + +### Supported Profile +* Tizen 10.0 RPI4 + +### Author +* Raunak Bhalotia diff --git a/Mobile/Downloader/Screenshots/Tizen/DownloadInfoPage.png b/Mobile/Downloader/Screenshots/Tizen/DownloadInfoPage.png new file mode 100755 index 000000000..42b8721e1 Binary files /dev/null and b/Mobile/Downloader/Screenshots/Tizen/DownloadInfoPage.png differ diff --git a/Mobile/Downloader/Screenshots/Tizen/DownloadMainPage.png b/Mobile/Downloader/Screenshots/Tizen/DownloadMainPage.png new file mode 100755 index 000000000..ff152c86e Binary files /dev/null and b/Mobile/Downloader/Screenshots/Tizen/DownloadMainPage.png differ diff --git a/Mobile/LeScanner/Lescanner.sln b/Mobile/LeScanner/Lescanner.sln new file mode 100644 index 000000000..b095480a7 --- /dev/null +++ b/Mobile/LeScanner/Lescanner.sln @@ -0,0 +1,22 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31005.135 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lescanner", "Lescanner/Lescanner.csproj", "{9fa85abf-475d-4461-9f8a-9ced53b3809c}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9fa85abf-475d-4461-9f8a-9ced53b3809c}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9fa85abf-475d-4461-9f8a-9ced53b3809c}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9fa85abf-475d-4461-9f8a-9ced53b3809c}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9fa85abf-475d-4461-9f8a-9ced53b3809c}.Release|Any CPU.Build.0 = Release|Any CPU + + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Mobile/LeScanner/Lescanner/AppStyles.cs b/Mobile/LeScanner/Lescanner/AppStyles.cs new file mode 100644 index 000000000..483941c62 --- /dev/null +++ b/Mobile/LeScanner/Lescanner/AppStyles.cs @@ -0,0 +1,40 @@ +using Tizen.NUI; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; + +namespace Lescanner +{ + /// + /// Centralized class for defining UI raw style data for a modern, clean application. + /// + public static class AppStyles + { + // Color Palette - Refined for elegance + public static readonly Color PrimaryColor = new Color(0.2f, 0.5f, 0.9f, 1.0f); // A softer, more sophisticated blue + public static readonly Color PrimaryColorDark = new Color(0.15f, 0.4f, 0.75f, 1.0f); // Darker blue for hover/pressed states + public static readonly Color SecondaryColor = new Color(0.97f, 0.97f, 0.99f, 1.0f); // A very clean, soft off-white for cards/sections + public static readonly Color PageBackgroundColor = new Color(0.99f, 0.99f, 1.0f, 1.0f); // Barely off-white for page backgrounds + public static readonly Color TextColorPrimary = new Color(0.15f, 0.15f, 0.2f, 1.0f); // Softer black for primary text + public static readonly Color TextColorSecondary = new Color(0.55f, 0.55f, 0.6f, 1.0f); // Muted grey for secondary text + public static readonly Color AppBarTextColor = Color.White; + public static readonly Color ButtonTextColor = Color.White; + public static readonly Color ListBackgroundColor = Color.White; // White for list item backgrounds + public static readonly Color BorderColor = new Color(0.88f, 0.88f, 0.92f, 1.0f); // A very light, subtle border color + + // Typography - Reduced for better readability + public static readonly float TitlePointSize = 28.0f; // For main page titles + public static readonly float HeaderPointSize = 24.0f; // For section headers or list item titles + public static readonly float BodyPointSize = 20.0f; // For standard body text, labels + public static readonly float DetailPointSize = 18.0f; // For smaller details, status text + + // Spacing & Sizing + public static readonly Extents PagePadding = new Extents(24, 24, 24, 24); + public static readonly Extents ComponentPadding = new Extents(20, 20, 20, 20); + public static readonly Extents ListElementPadding = new Extents(16, 16, 16, 16); + public static readonly Extents ListElementMargin = new Extents(0, 0, 12, 0); // Vertical spacing between list items + public static readonly Size2D LayoutCellPadding = new Size2D(0, 20); // Vertical spacing in layouts + public static readonly Extents ButtonMargin = new Extents(0, 0, 16, 0); // Margin at the bottom of buttons + public static readonly float CornerRadius = 10.0f; // Slightly smaller for a more refined look + public static readonly float TextFieldBorderWidth = 1.5f; // For text fields (if using a View as a border) + } +} diff --git a/Mobile/LeScanner/Lescanner/Constants.cs b/Mobile/LeScanner/Lescanner/Constants.cs new file mode 100644 index 000000000..76d64375a --- /dev/null +++ b/Mobile/LeScanner/Lescanner/Constants.cs @@ -0,0 +1,13 @@ +namespace Lescanner +{ + /// + /// Common constants used throughout the Lescanner application. + /// + public static class Constants + { + /// + /// Log tag used for all logging in the Lescanner application. + /// + public const string LOG_TAG = "Lescanner"; + } +} diff --git a/Mobile/LeScanner/Lescanner/DeviceListPage.cs b/Mobile/LeScanner/Lescanner/DeviceListPage.cs new file mode 100644 index 000000000..5a1da2543 --- /dev/null +++ b/Mobile/LeScanner/Lescanner/DeviceListPage.cs @@ -0,0 +1,364 @@ +using System; +using System.Collections.Generic; +using System.Threading; // Added for SynchronizationContext +using Tizen.NUI; +using Tizen.NUI.Components; +using Tizen.NUI.BaseComponents; +using Tizen.Network.Bluetooth; // For AdapterLeScanResultChangedEventArgs + +namespace Lescanner +{ + /// + /// Page to display a list of discovered BLE devices. + /// + class DeviceListPage : ContentPage + { + private TizenBLEService _bleService; + private Navigator _navigator; + private TextLabel _statusLabel; + private ScrollableBase _deviceScrollableList; + private View _deviceListContentContainer; + // Changed to store TextLabel to update it later + private Dictionary _discoveredDeviceLabels = new Dictionary(); + private bool _isScanActive = false; + private SynchronizationContext _uiContext; // Added for main thread dispatching + private HashSet _navigatedDevices = new HashSet(); // Track devices that have been navigated + + /// + /// Constructor for DeviceListPage. + /// + /// The BLE service instance. + /// The navigator for page navigation. + public DeviceListPage(TizenBLEService bleService, Navigator navigator) + { + _bleService = bleService; + _navigator = navigator; + _uiContext = SynchronizationContext.Current; // Capture main UI thread context + + AppBar = new AppBar { Title = "Device List" }; + + if (AppBar.Title != null) + { + AppBar.Title = AppBar.Title.PadLeft(20); + } + + // Subscribe to BLE service events + _bleService.DeviceDiscovered += OnDeviceDiscovered; + _bleService.GattConnectionStateChanged += OnGattConnectionStateChanged; // For connection feedback + _bleService.DeviceNameRetrieved += OnDeviceNameRetrieved; // Subscribe to name retrieval event + + InitializeComponent(); + + // Add back button to AppBar + AddBackButtonToAppBar(); + + // Start scanning process when the page is created. + StartScanningProcess(); + } + + /// + /// Adds a back button to the AppBar. + /// + private void AddBackButtonToAppBar() + { + if (AppBar != null) + { + // Create a back button + var backButton = new Button() + { + Text = "Back", + WidthSpecification = 80, + HeightSpecification = 40 + }; + + // Set button background color + backButton.BackgroundColor = new Color(0.2f, 0.6f, 1.0f, 1.0f); + backButton.TextColor = Color.White; + + // Add click handler + backButton.Clicked += OnBackButtonClicked; + + // Add button to the AppBar + AppBar.NavigationContent = backButton; + } + } + + /// + /// Initializes the UI components for the page. + /// + private void InitializeComponent() + { + var mainLayoutContainer = Resources.CreateMainLayoutContainer(); + Content = mainLayoutContainer; + + _statusLabel = Resources.CreateDetailLabel("Preparing to scan..."); + _statusLabel.BackgroundColor = Color.Transparent; + mainLayoutContainer.Add(_statusLabel); + + _deviceListContentContainer = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = new Size2D(0, AppStyles.LayoutCellPadding.Height) // Use style for spacing + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + + _deviceScrollableList = Resources.CreateScrollableList(); + _deviceScrollableList.Add(_deviceListContentContainer); + mainLayoutContainer.Add(_deviceScrollableList); + } + + + private async void StartScanningProcess() + { + ClearDeviceList(); + _statusLabel.Text = "Scanning for devices..."; + _isScanActive = true; + await _bleService.StartLeScanAsync(); + + // Simulate a 30-second scan duration like the original app + await System.Threading.Tasks.Task.Delay(30000); + + if (_isScanActive) // Check if still active (e.g., user didn't navigate away) + { + await StopScanningProcessAsync(); + } + } + + private async System.Threading.Tasks.Task StopScanningProcessAsync() + { + if (_isScanActive) + { + _isScanActive = false; + await _bleService.StopLeScanAsync(); + _statusLabel.Text = "Scan completed. Tap a device to connect."; + } + } + + private void ClearDeviceList() + { + _discoveredDeviceLabels.Clear(); + while (_deviceListContentContainer.ChildCount > 0) + { + _deviceListContentContainer.Remove(_deviceListContentContainer.GetChildAt(0)); + } + } + + /// + /// Event handler for when a new device is discovered by the BLE service. + /// + private void OnDeviceDiscovered(object sender, AdapterLeScanResultChangedEventArgs e) + { + if (e.DeviceData != null && !string.IsNullOrEmpty(e.DeviceData.RemoteAddress)) + { + // Ensure UI updates happen on the main thread + _uiContext.Post(_ => + { + if (!_discoveredDeviceLabels.ContainsKey(e.DeviceData.RemoteAddress)) + { + string initialName = null; + try + { + // Attempt to get the name from scan data using a default packet type (0). + // This might resolve the obsolete warning and provide an early name. + initialName = e.DeviceData.GetDeviceName(0); + } + catch (Exception ex) + { + Tizen.Log.Warn(Constants.LOG_TAG, $"Error calling GetDeviceName(0) for {e.DeviceData.RemoteAddress}: {ex.Message}. Will use address."); + } + AddDeviceToList(e.DeviceData.RemoteAddress, initialName); + Tizen.Log.Info(Constants.LOG_TAG, $"Device discovered: {e.DeviceData.RemoteAddress}. Initial name from scan: {initialName ?? "N/A"}"); + } + }, null); + } + } + + private void AddDeviceToList(string deviceAddress, string initialName) // Accept address and initial name + { + var deviceItemContainer = Resources.CreateListItemContainer(); + deviceItemContainer.Padding = AppStyles.ListElementPadding; + + // Show initial name if available, otherwise show address. + string initialText = string.IsNullOrEmpty(initialName) ? deviceAddress : $"{initialName} ({deviceAddress})"; + var deviceLabel = Resources.CreateBodyLabel(initialText); + + // Make it look tappable + deviceLabel.TouchEvent += (s, args) => + { + if (args.Touch.GetState(0) == PointStateType.Down) + { + OnDeviceTapped(deviceAddress); + } + return true; + }; + + _discoveredDeviceLabels.Add(deviceAddress, deviceLabel); // Store the label + deviceItemContainer.Add(deviceLabel); + _deviceListContentContainer.Add(deviceItemContainer); + } + + /// + /// Event handler for when a device name is retrieved after connection. + /// This handles navigation to ensure we navigate only once with the proper device name. + /// + private void OnDeviceNameRetrieved(object sender, DeviceNameEventArgs e) + { + try + { + _uiContext.Post(_ => + { + try + { + if (_discoveredDeviceLabels.TryGetValue(e.DeviceAddress, out TextLabel deviceLabel)) + { + if (!string.IsNullOrEmpty(e.DeviceName)) + { + deviceLabel.Text = $"{e.DeviceName} ({e.DeviceAddress})"; + Tizen.Log.Info(Constants.LOG_TAG, $"Updated UI for {e.DeviceAddress} with name: {e.DeviceName}"); + + // Safely update status label to show device name was updated + try + { + _statusLabel.Text = $"Updated device name: {e.DeviceName}"; + } + catch (Exception statusEx) + { + Tizen.Log.Warn(Constants.LOG_TAG, $"Could not update status label: {statusEx.Message}"); + } + } + else + { + // Name might be null if not found or failed to read, keep address only. + Tizen.Log.Info(Constants.LOG_TAG, $"Failed to retrieve name for {e.DeviceAddress}. UI remains as address."); + } + + // Navigate to UUID page only when we have a valid device name (not null) + if (!string.IsNullOrEmpty(e.DeviceName) && !_navigatedDevices.Contains(e.DeviceAddress)) + { + _navigatedDevices.Add(e.DeviceAddress); + string displayName = deviceLabel.Text; + Tizen.Log.Info(Constants.LOG_TAG, $"Device name '{e.DeviceName}' retrieved for {e.DeviceAddress}. Navigating to UUID page with display name: {displayName}"); + _navigator.Push(new UuidListPage(_bleService, _navigator, e.DeviceAddress, displayName)); + } + else if (string.IsNullOrEmpty(e.DeviceName)) + { + Tizen.Log.Info(Constants.LOG_TAG, $"Device name is null or empty for {e.DeviceAddress}. Waiting for name retrieval before navigation."); + } + else + { + Tizen.Log.Info(Constants.LOG_TAG, $"Already navigated for {e.DeviceAddress}. Skipping duplicate navigation."); + } + } + else + { + Tizen.Log.Warn(Constants.LOG_TAG, $"Received name for {e.DeviceAddress} but it's not in the discovered list (maybe navigated away?)."); + } + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Exception in OnDeviceNameRetrieved UI thread for {e.DeviceAddress}: {ex.Message}"); + } + }, null); + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Exception in OnDeviceNameRetrieved for {e.DeviceAddress}: {ex.Message}"); + } + } + + /// + /// Handles the tap event on a device in the list. + /// + /// The address of the tapped device. + private async void OnDeviceTapped(string deviceAddress) + { + try + { + Tizen.Log.Info(Constants.LOG_TAG, $"Device tapped: {deviceAddress}. Initiating GATT connection."); + _statusLabel.Text = $"Connecting to {deviceAddress}..."; + + // Stop scanning before attempting connection + if (_isScanActive) + { + await StopScanningProcessAsync(); + } + + // Clear any previous navigation state for this device + _navigatedDevices.Remove(deviceAddress); + + // Initiate the connection. Navigation will be handled by OnDeviceNameRetrieved. + bool connectionResult = await _bleService.ConnectGattAsync(deviceAddress); + + if (!connectionResult) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Failed to initiate connection to {deviceAddress}"); + _statusLabel.Text = $"Failed to connect, retrying..."; + } + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Exception in OnDeviceTapped for {deviceAddress}: {ex.Message}"); + _statusLabel.Text = $"Connection error: {ex.Message}"; + } + } + + /// + /// Event handler for GATT connection state changes (for feedback on this page if needed). + /// + private void OnGattConnectionStateChanged(object sender, GattConnectionStateChangedEventArgs e) + { + try + { + _uiContext.Post(_ => + { + try + { + if (e.IsConnected) + { + _statusLabel.Text = "GATT Connected. Fetching device name..."; + // Navigation will be handled by OnDeviceNameRetrieved to ensure we have the device name + } + else + { + _statusLabel.Text = "GATT Disconnected or connection failed."; + } + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Exception in OnGattConnectionStateChanged UI thread: {ex.Message}"); + } + }, null); + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Exception in OnGattConnectionStateChanged: {ex.Message}"); + } + } + + /// + /// Event handler for the custom back button click. + /// Stops the scanning process and navigates back to the previous page. + /// + /// The event source. + /// The event arguments. + private async void OnBackButtonClicked(object sender, ClickedEventArgs e) + { + Tizen.Log.Info("LescannerDeviceListPage", "Back button clicked. Stopping scan and navigating back."); + + // Stop the scanning process immediately + if (_isScanActive) + { + _statusLabel.Text = "Stopping scan..."; + await StopScanningProcessAsync(); + } + + // Navigate back to the previous page + _navigator.Pop(); + } + + } +} diff --git a/Mobile/LeScanner/Lescanner/Directory.Build.targets b/Mobile/LeScanner/Lescanner/Directory.Build.targets new file mode 100644 index 000000000..49b3ab641 --- /dev/null +++ b/Mobile/LeScanner/Lescanner/Directory.Build.targets @@ -0,0 +1,21 @@ + + + + + + + + $([System.IO.Path]::GetDirectoryName($(MSBuildProjectDirectory))) + + + + + + diff --git a/Mobile/LeScanner/Lescanner/HomePage.cs b/Mobile/LeScanner/Lescanner/HomePage.cs new file mode 100644 index 000000000..d93c02a4b --- /dev/null +++ b/Mobile/LeScanner/Lescanner/HomePage.cs @@ -0,0 +1,74 @@ +using System; +using Tizen.NUI; +using Tizen.NUI.Components; +using Tizen.NUI.BaseComponents; + +namespace Lescanner +{ + /// + /// The home page for the LE Scanner application. + /// Contains a button to initiate BLE scanning. + /// + class HomePage : ContentPage + { + private TizenBLEService _bleService; + private Navigator _navigator; + private Button _scanButton; + private TextLabel _statusLabel; + + /// + /// Constructor for HomePage. + /// + /// The BLE service instance. + /// The navigator for page navigation. + public HomePage(TizenBLEService bleService, Navigator navigator) + { + _bleService = bleService; + _navigator = navigator; + AppBar = new AppBar { Title = "LE Scanner" }; + InitializeComponent(); + } + + /// + /// Initializes the UI components for the page. + /// + private void InitializeComponent() + { + var mainLayoutContainer = Resources.CreateMainLayoutContainer(); + Content = mainLayoutContainer; + + var titleLabel = Resources.CreateTitleLabel("Bluetooth LE Scanner"); + mainLayoutContainer.Add(titleLabel); + + _scanButton = Resources.CreatePrimaryButton("BLE Scan"); + _scanButton.Clicked += OnScanButtonClicked; + mainLayoutContainer.Add(_scanButton); + + _statusLabel = Resources.CreateDetailLabel("Tap 'BLE Scan' to start."); + _statusLabel.BackgroundColor = Color.Transparent; // Ensure background is transparent + mainLayoutContainer.Add(_statusLabel); + } + + /// + /// Handles the click event for the BLE Scan button. + /// + /// The event source. + /// The event arguments. + private void OnScanButtonClicked(object sender, ClickedEventArgs e) + { + Tizen.Log.Info(Constants.LOG_TAG, "BLE Scan button clicked."); + if (_bleService.IsBluetoothEnabled()) + { + _statusLabel.Text = "Tap 'BLE Scan' to start."; + // Navigate to DeviceListPage. The page will handle starting the scan. + _navigator.Push(new DeviceListPage(_bleService, _navigator)); + } + else + { + _statusLabel.Text = "Please turn on Bluetooth."; + // Optionally, show a more persistent message or a Toast. + Tizen.Log.Warn(Constants.LOG_TAG, "Bluetooth is not enabled."); + } + } + } +} diff --git a/Mobile/LeScanner/Lescanner/Lescanner.cs b/Mobile/LeScanner/Lescanner/Lescanner.cs new file mode 100644 index 000000000..0122f18ee --- /dev/null +++ b/Mobile/LeScanner/Lescanner/Lescanner.cs @@ -0,0 +1,42 @@ +using System; +using Tizen.NUI; +using Tizen.NUI.Components; +using Tizen.NUI.BaseComponents; + +namespace Lescanner +{ + class Lescanner : NUIApplication + { + private TizenBLEService _bleService; + + /// + /// Overrides the base class method to create the window and initialize the application. + /// + protected override void OnCreate() + { + base.OnCreate(); + NUIApplication.IsUsingXaml = false; + + Window window = Window.Default; + window.WindowSize = new Size2D(720, 1280); // Default size, can be adjusted + window.Title = "LE Scanner"; + window.SetFullScreen(true); // Make the application fullscreen + + _bleService = new TizenBLEService(); + + // Create a Navigator and push the HomePage as the initial page + var navigator = window.GetDefaultNavigator(); + navigator.Push(new HomePage(_bleService, navigator)); + } + + /// + /// The main entry point for the application. + /// + /// Arguments. + static void Main(string[] args) + { + var app = new Lescanner(); + app.Run(args); + } + } +} diff --git a/Mobile/LeScanner/Lescanner/Lescanner.csproj b/Mobile/LeScanner/Lescanner/Lescanner.csproj new file mode 100644 index 000000000..64b007b4e --- /dev/null +++ b/Mobile/LeScanner/Lescanner/Lescanner.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0-tizen9.0 + + + + portable + + + None + + + + + + + + diff --git a/Mobile/LeScanner/Lescanner/Resources.cs b/Mobile/LeScanner/Lescanner/Resources.cs new file mode 100644 index 000000000..09aeb0ca8 --- /dev/null +++ b/Mobile/LeScanner/Lescanner/Resources.cs @@ -0,0 +1,222 @@ +using Tizen.NUI; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; + +namespace Lescanner +{ + /// + /// Static helper class for creating pre-styled UI components using raw data from AppStyles. + /// This promotes consistency and reduces boilerplate code in page definitions. + /// + public static class Resources + { + /// + /// Creates a TextLabel styled as a main title. + /// + /// The text for the label. + /// A styled TextLabel. + public static TextLabel CreateTitleLabel(string text) + { + return new TextLabel + { + Text = text, + TextColor = AppStyles.TextColorPrimary, + PointSize = AppStyles.TitlePointSize, + HorizontalAlignment = HorizontalAlignment.Center, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + } + + /// + /// Creates a TextLabel styled for headers or list item titles. + /// + /// The text for the label. + /// A styled TextLabel. + public static TextLabel CreateHeaderLabel(string text) + { + return new TextLabel + { + Text = text, + TextColor = AppStyles.TextColorPrimary, + PointSize = AppStyles.HeaderPointSize, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + } + + /// + /// Creates a TextLabel styled for body text. + /// + /// The text for the label. + /// A styled TextLabel. + public static TextLabel CreateBodyLabel(string text) + { + return new TextLabel + { + Text = text, + TextColor = AppStyles.TextColorPrimary, + PointSize = AppStyles.BodyPointSize, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + } + + /// + /// Creates a TextLabel styled for detail or secondary text. + /// + /// The text for the label. + /// A styled TextLabel. + public static TextLabel CreateDetailLabel(string text) + { + return new TextLabel + { + Text = text, + TextColor = AppStyles.TextColorSecondary, + PointSize = AppStyles.DetailPointSize, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + } + + /// + /// Creates a Button styled as a primary action button. + /// + /// The text for the button. + /// A styled Button. + public static Button CreatePrimaryButton(string text) + { + var button = new Button + { + Text = text, + TextColor = AppStyles.ButtonTextColor, + PointSize = AppStyles.BodyPointSize, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = 72, // Fixed height for better touch experience + Margin = AppStyles.ButtonMargin, + BackgroundColor = AppStyles.PrimaryColor, + CornerRadius = AppStyles.CornerRadius, + }; + return button; + } + + /// + /// Creates a Button styled as a secondary action button. + /// + /// The text for the button. + /// A styled Button. + public static Button CreateSecondaryButton(string text) + { + var button = new Button + { + Text = text, + TextColor = AppStyles.PrimaryColor, + PointSize = AppStyles.BodyPointSize, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = 72, + Margin = AppStyles.ButtonMargin, + BackgroundColor = Color.Transparent, // Transparent background + CornerRadius = AppStyles.CornerRadius, + }; + // To create a border effect for a secondary button, we can wrap it in a View + // or use a background image. For simplicity, we'll keep it without a border for now. + return button; + } + + /// + /// Creates a View styled as a container for list items or cards. + /// + /// A styled View. + public static View CreateListItemContainer() + { + return new View + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + BackgroundColor = AppStyles.ListBackgroundColor, + CornerRadius = AppStyles.CornerRadius, + Margin = AppStyles.ListElementMargin, + // Padding = AppStyles.ListElementPadding // Padding will be handled by child elements or specific cases + }; + } + + /// + /// Creates a ScrollableBase styled for displaying lists. + /// + /// A styled ScrollableBase. + public static ScrollableBase CreateScrollableList() + { + return new ScrollableBase + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + ScrollingDirection = ScrollableBase.Direction.Vertical, + HideScrollbar = false, + BackgroundColor = Color.Transparent, + }; + } + + /// + /// Creates a main content View with a vertical linear layout. + /// + /// Optional padding to override the default page padding. + /// A styled View. + public static View CreateMainLayoutContainer(Extents padding = null) + { + var layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = AppStyles.LayoutCellPadding + }; + var view = new View + { + Layout = layout, + BackgroundColor = AppStyles.PageBackgroundColor, + Padding = padding ?? AppStyles.PagePadding + }; + view.WidthSpecification = LayoutParamPolicies.MatchParent; + view.HeightSpecification = LayoutParamPolicies.MatchParent; + return view; + } + + /// + /// Creates a TextField with a modern look, wrapped in a View to simulate a border. + /// + /// The placeholder text. + /// A View containing the styled TextField. + public static View CreateStyledTextField(string placeholderText) + { + var textField = new TextField + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = 64, // Fixed height + PlaceholderText = placeholderText, + BackgroundColor = Color.Transparent, // Transparent, border is handled by parent + TextColor = AppStyles.TextColorPrimary, + PointSize = AppStyles.BodyPointSize, + Padding = new Extents(16, 16, 16, 16), // Internal text padding + // Margin is handled by the container + }; + + var borderContainer = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + HorizontalAlignment = HorizontalAlignment.Begin, + VerticalAlignment = VerticalAlignment.Center + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = 64, // Match TextField height + BackgroundColor = AppStyles.ListBackgroundColor, // White background for the field + CornerRadius = AppStyles.CornerRadius, + Margin = new Extents(0, 0, 16, 0), // Bottom margin for the container + // To simulate a border, we can use a slightly larger background View with a different color + // or use a BorderImage if available. For simplicity, we'll rely on CornerRadius for now. + // A more robust border would require a custom drawing or a 9-patch image. + }; + borderContainer.Add(textField); + return borderContainer; + } + } +} diff --git a/Mobile/LeScanner/Lescanner/TizenBLEService.cs b/Mobile/LeScanner/Lescanner/TizenBLEService.cs new file mode 100644 index 000000000..d491d2ddd --- /dev/null +++ b/Mobile/LeScanner/Lescanner/TizenBLEService.cs @@ -0,0 +1,429 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; // For Encoding +using System.Threading.Tasks; +using Tizen.Network.Bluetooth; + +namespace Lescanner +{ + /// + /// Event arguments for when a device name is retrieved. + /// + public class DeviceNameEventArgs : EventArgs + { + public string DeviceAddress { get; } + public string DeviceName { get; } + + public DeviceNameEventArgs(string deviceAddress, string deviceName) + { + DeviceAddress = deviceAddress; + DeviceName = deviceName; + } + } + + /// + /// Service class to handle Bluetooth Low Energy operations. + /// Encapsulates Tizen.Network.Bluetooth logic. + /// + public class TizenBLEService + { + // Event to notify when a new device is discovered + public event EventHandler DeviceDiscovered; + // Event to notify when GATT connection state changes + public event EventHandler GattConnectionStateChanged; + // Event to notify when service discovery is complete + public event EventHandler> ServicesDiscovered; + // Event to notify when device name is retrieved after connection + public event EventHandler DeviceNameRetrieved; + + private BluetoothGattClient _gattClient; + private bool _isScanning = false; + private string _currentConnectedDeviceAddress; // To store address of the connected device for name fetching + + /// + /// Gets the address of the currently connected device. + /// + public string CurrentConnectedDeviceAddress => _currentConnectedDeviceAddress; + + /// + /// Gets the currently discovered services for the connected device. + /// + /// List of service UUIDs, or empty list if not connected or no services discovered. + public IEnumerable GetCurrentServices() + { + if (_gattClient == null) + { + return new List(); + } + + try + { + var services = _gattClient.GetServices(); + if (services != null) + { + return services.Select(s => s.Uuid).ToList(); + } + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Error getting current services: {ex.Message}"); + } + + return new List(); + } + + // Standard UUIDs for Generic Access service and Device Name characteristic + private const string GENERIC_ACCESS_SERVICE_UUID = "00001800-0000-1000-8000-00805f9b34fb"; + private const string DEVICE_NAME_CHARACTERISTIC_UUID = "00002a00-0000-1000-8000-00805f9b34fb"; + + /// + /// Checks if Bluetooth is enabled on the device. + /// + /// True if Bluetooth is enabled, false otherwise. + public bool IsBluetoothEnabled() + { + try + { + return BluetoothAdapter.IsBluetoothEnabled; + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Error checking Bluetooth status: {ex.Message}"); + return false; + } + } + + /// + /// Starts the LE scan process. + /// + public async Task StartLeScanAsync() + { + if (_isScanning) + { + Tizen.Log.Info(Constants.LOG_TAG, "Already scanning."); + return; + } + + if (!IsBluetoothEnabled()) + { + Tizen.Log.Warn(Constants.LOG_TAG, "Bluetooth is not enabled. Cannot start scan."); + return; + } + + try + { + _isScanning = true; + BluetoothAdapter.ScanResultChanged += OnScanResultChanged; + BluetoothAdapter.StartLeScan(); + Tizen.Log.Info(Constants.LOG_TAG, "LE Scan started."); + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Error starting LE scan: {ex.Message}. The Bluetooth adapter may be in a bad state. Please try restarting Bluetooth or the device."); + _isScanning = false; + BluetoothAdapter.ScanResultChanged -= OnScanResultChanged; + } + } + + /// + /// Stops the LE scan process. + /// + public async Task StopLeScanAsync() + { + if (!_isScanning) + { + Tizen.Log.Info(Constants.LOG_TAG, "Not currently scanning."); + return; + } + + try + { + BluetoothAdapter.StopLeScan(); + Tizen.Log.Info(Constants.LOG_TAG, "LE Scan stopped."); + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Error stopping LE scan: {ex.Message}"); + } + finally + { + _isScanning = false; + BluetoothAdapter.ScanResultChanged -= OnScanResultChanged; + } + } + + /// + /// Event handler for BluetoothAdapter.ScanResultChanged. + /// + private void OnScanResultChanged(object sender, AdapterLeScanResultChangedEventArgs e) + { + DeviceDiscovered?.Invoke(this, e); + } + + private bool _isConnecting = false; // Track connection state + + /// + /// Initiates a GATT connection to the specified device address. + /// + /// The Bluetooth address of the device. + /// True if connection initiation was successful, false otherwise. + public async Task ConnectGattAsync(string deviceAddress) + { + if (_isConnecting) + { + Tizen.Log.Warn(Constants.LOG_TAG, "Connection already in progress. Please wait."); + return false; + } + + if (_gattClient != null) + { + Tizen.Log.Warn(Constants.LOG_TAG, "A GATT client already exists. Disconnecting first."); + await DisconnectGattAsync(); + + // Add a small delay to ensure the disconnect is fully processed + await Task.Delay(500); + } + + try + { + _isConnecting = true; + _currentConnectedDeviceAddress = deviceAddress; // Store for name fetching + _gattClient = BluetoothGattClient.CreateClient(deviceAddress); + if (_gattClient == null) + { + Tizen.Log.Error(Constants.LOG_TAG, "Failed to create GATT client."); + _currentConnectedDeviceAddress = null; + _isConnecting = false; + return false; + } + _gattClient.ConnectionStateChanged += OnGattConnectionStateChanged; + await _gattClient.ConnectAsync(false); // false for direct connection + Tizen.Log.Info(Constants.LOG_TAG, $"GATT connection initiated to {deviceAddress}."); + return true; + } + catch (InvalidOperationException ex) when (ex.Message.Contains("Operation now in progress")) + { + // This specific error often means a connection attempt is already underway. + // Do not dispose of the client yet, let the ConnectionStateChanged event determine the final state. + Tizen.Log.Warn(Constants.LOG_TAG, $"GATT connection to {deviceAddress} reported 'Operation now in progress'. Awaiting ConnectionStateChanged event. Message: {ex.Message}"); + // The client might still be valid, so we don't dispose it here. + // Return false to indicate the ConnectAsync call itself had issues. + _isConnecting = false; + return false; + } + catch (InvalidOperationException ex) when (ex.Message.Contains("Operation already done")) + { + Tizen.Log.Warn(Constants.LOG_TAG, $"GATT connection to {deviceAddress} reported 'Operation already done'. The connection might already be established. Message: {ex.Message}"); + _isConnecting = false; + return false; + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Error connecting GATT to {deviceAddress}: {ex.Message}"); + _gattClient?.Dispose(); + _gattClient = null; + _currentConnectedDeviceAddress = null; + _isConnecting = false; + return false; + } + } + + /// + /// Disconnects the current GATT client with a timeout. + /// Event is unsubscribed at the beginning of the process to prevent interference with new connections, + /// and to ensure the handler is not attached to the client when it's disposed. + /// + public async Task DisconnectGattAsync() + { + if (_gattClient == null) + { + Tizen.Log.Info(Constants.LOG_TAG, "No GATT client to disconnect."); + return; + } + + var clientToDispose = _gattClient; + + try + { + // Set to null first to prevent reconnection attempts during disconnect + _gattClient = null; + _currentConnectedDeviceAddress = null; + + // Unsubscribe first to prevent this handler from being called with a disposed object later, + // and to avoid issues when creating a new client. + clientToDispose.ConnectionStateChanged -= OnGattConnectionStateChanged; + Tizen.Log.Info(Constants.LOG_TAG, "Unsubscribed from ConnectionStateChanged event. Proceeding with disconnect."); + + var disconnectTask = clientToDispose.DisconnectAsync(); + var timeoutTask = Task.Delay(3000); // Reduced to 3 seconds + + if (await Task.WhenAny(disconnectTask, timeoutTask) == timeoutTask) + { + Tizen.Log.Warn(Constants.LOG_TAG, "GATT disconnect timed out after 3 seconds. Forcing disposal."); + } + else + { + Tizen.Log.Info(Constants.LOG_TAG, "GATT disconnected."); + } + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Error during GATT disconnect: {ex.Message}"); + } + finally + { + try + { + clientToDispose?.Dispose(); + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Error disposing GATT client: {ex.Message}"); + } + } + } + + /// + /// Event handler for GattClient.ConnectionStateChanged. + /// + private void OnGattConnectionStateChanged(object sender, GattConnectionStateChangedEventArgs e) + { + // Robustness check: Ensure the sender is the currently active client. + // This prevents processing stale events from a client that was just disposed. + if (_gattClient == null || sender != _gattClient) + { + Tizen.Log.Warn(Constants.LOG_TAG, "Received ConnectionStateChanged event for a stale or null client. Ignoring."); + return; + } + + // Robustness check: Ensure event args are valid. + if (e == null) + { + Tizen.Log.Warn(Constants.LOG_TAG, "ConnectionStateChanged event received with null args. Ignoring."); + return; + } + + Tizen.Log.Info(Constants.LOG_TAG, $"ConnectionStateChanged: IsConnected = {e.IsConnected}, RemoteAddress = {_gattClient.RemoteAddress}"); + GattConnectionStateChanged?.Invoke(this, e); + + if (e.IsConnected) + { + _currentConnectedDeviceAddress = _gattClient.RemoteAddress; // Update address on successful connection + _isConnecting = false; // Clear connecting flag on successful connection + Tizen.Log.Info(Constants.LOG_TAG, "GATT connected. Discovering services and fetching name."); + DiscoverServices(); + _ = FetchDeviceNameAsync(); // Fire and forget + } + else + { + Tizen.Log.Info(Constants.LOG_TAG, "GATT disconnected."); + _isConnecting = false; // Clear connecting flag on disconnect + // Do not set _gattClient to null here, DisconnectGattAsync handles it. + // Only clear the connected address. + _currentConnectedDeviceAddress = null; + } + } + + + /// + /// Fetches the device name by reading the Device Name characteristic from the Generic Access service. + /// Based on TizenFX source, ReadValueAsync returns a Task and the value is updated on the characteristic object. + /// + private async Task FetchDeviceNameAsync() + { + if (_gattClient == null || string.IsNullOrEmpty(_currentConnectedDeviceAddress)) + { + Tizen.Log.Warn(Constants.LOG_TAG, "GATT client or device address is null. Cannot fetch name."); + return; + } + + try + { + BluetoothGattService genericAccessService = _gattClient.GetService(GENERIC_ACCESS_SERVICE_UUID); + if (genericAccessService != null) + { + Tizen.Log.Info(Constants.LOG_TAG, "Found Generic Access service."); + BluetoothGattCharacteristic deviceNameCharacteristic = genericAccessService.GetCharacteristic(DEVICE_NAME_CHARACTERISTIC_UUID); + if (deviceNameCharacteristic != null) + { + Tizen.Log.Info(Constants.LOG_TAG, "Found Device Name characteristic. Initiating read..."); + // The ReadValueAsync method will complete when the read operation is done. + // The value of the characteristic will be updated internally. + bool readSuccess = await _gattClient.ReadValueAsync(deviceNameCharacteristic); + + if (readSuccess) + { + Tizen.Log.Info(Constants.LOG_TAG, "ReadValueAsync reported success. Checking characteristic value."); + if (deviceNameCharacteristic.Value != null) + { + string deviceName = Encoding.UTF8.GetString(deviceNameCharacteristic.Value).TrimEnd('\0'); + Tizen.Log.Info(Constants.LOG_TAG, $"Successfully fetched device name: {deviceName}"); + DeviceNameRetrieved?.Invoke(this, new DeviceNameEventArgs(_currentConnectedDeviceAddress, deviceName)); + } + else + { + Tizen.Log.Warn(Constants.LOG_TAG, "ReadValueAsync reported success, but characteristic value is null."); + DeviceNameRetrieved?.Invoke(this, new DeviceNameEventArgs(_currentConnectedDeviceAddress, null)); + } + } + else + { + Tizen.Log.Error(Constants.LOG_TAG, "ReadValueAsync failed to read the Device Name characteristic."); + DeviceNameRetrieved?.Invoke(this, new DeviceNameEventArgs(_currentConnectedDeviceAddress, null)); + } + } + else + { + Tizen.Log.Warn(Constants.LOG_TAG, "Device Name characteristic not found."); + DeviceNameRetrieved?.Invoke(this, new DeviceNameEventArgs(_currentConnectedDeviceAddress, null)); + } + } + else + { + Tizen.Log.Warn(Constants.LOG_TAG, "Generic Access service not found."); + DeviceNameRetrieved?.Invoke(this, new DeviceNameEventArgs(_currentConnectedDeviceAddress, null)); + } + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Error fetching device name: {ex.Message}"); + DeviceNameRetrieved?.Invoke(this, new DeviceNameEventArgs(_currentConnectedDeviceAddress, null)); + } + } + + + /// + /// Discovers services offered by the connected GATT server. + /// + private void DiscoverServices() + { + if (_gattClient == null) + { + Tizen.Log.Warn(Constants.LOG_TAG, "GATT client is null. Cannot discover services."); + return; + } + + try + { + IEnumerable services = _gattClient.GetServices(); + var uuids = new List(); + if (services != null) + { + foreach (var service in services) + { + uuids.Add(service.Uuid); + Tizen.Log.Info(Constants.LOG_TAG, $"Discovered service UUID: {service.Uuid}"); + } + } + ServicesDiscovered?.Invoke(this, uuids); + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Error discovering services: {ex.Message}"); + ServicesDiscovered?.Invoke(this, new List()); // Notify with empty list on error + } + } + + } +} diff --git a/Mobile/LeScanner/Lescanner/UuidListPage.cs b/Mobile/LeScanner/Lescanner/UuidListPage.cs new file mode 100644 index 000000000..e9a02a7df --- /dev/null +++ b/Mobile/LeScanner/Lescanner/UuidListPage.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; // Added for SynchronizationContext +using Tizen.NUI; +using Tizen.NUI.Components; +using Tizen.NUI.BaseComponents; +using Tizen.Network.Bluetooth; // For GattConnectionStateChangedEventArgs + +namespace Lescanner +{ + /// + /// Page to display a list of discovered service UUIDs for a connected BLE device. + /// + class UuidListPage : ContentPage + { + private TizenBLEService _bleService; + private Navigator _navigator; + private string _deviceAddress; + private string _deviceDisplayName; // Added to store the fetched display name + private TextLabel _statusLabel; + private ScrollableBase _uuidScrollableList; + private View _uuidListContentContainer; + private SynchronizationContext _uiContext; // Added for main thread dispatching + + /// + /// Constructor for UuidListPage. + /// + /// The BLE service instance. + /// The navigator for page navigation. + /// The address of the connected device. + /// The display name (address or name) of the connected device. + public UuidListPage(TizenBLEService bleService, Navigator navigator, string deviceAddress, string displayName) + { + _bleService = bleService; + _navigator = navigator; + _deviceAddress = deviceAddress; + _deviceDisplayName = displayName ?? deviceAddress; // Fallback to address if displayName is null + _uiContext = SynchronizationContext.Current; // Capture main UI thread context + AppBar = new AppBar { Title = $"UUIDs for {_deviceDisplayName}" }; // Use display name + InitializeComponent(); + + // Subscribe to BLE service events + _bleService.GattConnectionStateChanged += OnGattConnectionStateChanged; + _bleService.ServicesDiscovered += OnServicesDiscovered; + + // Check if we're already connected and services might already be discovered + CheckExistingConnectionAndServices(); + } + + /// + /// Override Dispose to properly unsubscribe from events + /// + protected override void Dispose(DisposeTypes type) + { + try + { + if (_bleService != null) + { + _bleService.GattConnectionStateChanged -= OnGattConnectionStateChanged; + _bleService.ServicesDiscovered -= OnServicesDiscovered; + Tizen.Log.Info(Constants.LOG_TAG, "Unsubscribed from BLE service events."); + } + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Error unsubscribing from BLE service events: {ex.Message}"); + } + + base.Dispose(type); + } + + /// + /// Initializes the UI components for the page. + /// + private void InitializeComponent() + { + var mainLayoutContainer = Resources.CreateMainLayoutContainer(); + Content = mainLayoutContainer; + + _statusLabel = Resources.CreateDetailLabel($"Connecting to {_deviceDisplayName}..."); // Use display name + _statusLabel.BackgroundColor = Color.Transparent; + mainLayoutContainer.Add(_statusLabel); + + _uuidListContentContainer = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = new Size2D(0, AppStyles.LayoutCellPadding.Height) // Use style for spacing + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + + _uuidScrollableList = Resources.CreateScrollableList(); + _uuidScrollableList.Add(_uuidListContentContainer); + mainLayoutContainer.Add(_uuidScrollableList); + } + + + /// + /// Event handler for GATT connection state changes. + /// + private void OnGattConnectionStateChanged(object sender, GattConnectionStateChangedEventArgs e) + { + // Ensure UI updates happen on the main thread + _uiContext.Post(_ => // Changed from NUIApplication.GetDefaultWindow().Post + { + if (e.IsConnected) + { + _statusLabel.Text = $"Connected to {_deviceDisplayName}. Fetching services..."; // Use display name + Tizen.Log.Info(Constants.LOG_TAG, $"GATT connected to {_deviceAddress} ({_deviceDisplayName})."); + } + else + { + _statusLabel.Text = $"Disconnected from {_deviceDisplayName}."; // Use display name + Tizen.Log.Warn(Constants.LOG_TAG, $"GATT disconnected from {_deviceAddress} ({_deviceDisplayName})."); + // Optionally, navigate back or show an error. + } + }, null); // Added null for state parameter + } + + /// + /// Event handler for when services are discovered. + /// + private void OnServicesDiscovered(object sender, IEnumerable uuids) + { + // Ensure UI updates happen on the main thread + _uiContext.Post(_ => // Changed from NUIApplication.GetDefaultWindow().Post + { + Tizen.Log.Info(Constants.LOG_TAG, $"Services discovered event received for {_deviceDisplayName}. Count: {uuids?.Count() ?? 0}"); + ClearUuidList(); + if (uuids != null && uuids.Any()) + { + _statusLabel.Text = $"Found {uuids.Count()} services for {_deviceDisplayName}:"; // Use display name + foreach (var uuid in uuids) + { + AddUuidToList(uuid); + } + } + else + { + _statusLabel.Text = $"No services found for {_deviceDisplayName}."; // Use display name + } + }, null); // Added null for state parameter + } + + private void ClearUuidList() + { + while (_uuidListContentContainer.ChildCount > 0) + { + _uuidListContentContainer.Remove(_uuidListContentContainer.GetChildAt(0)); + } + } + + private void AddUuidToList(string uuid) + { + var uuidItemContainer = Resources.CreateListItemContainer(); + uuidItemContainer.Padding = AppStyles.ListElementPadding; + + var uuidLabel = Resources.CreateBodyLabel(uuid); + uuidLabel.TextColor = AppStyles.TextColorSecondary; // Use secondary color for UUIDs + + uuidItemContainer.Add(uuidLabel); + _uuidListContentContainer.Add(uuidItemContainer); + } + + /// + /// Checks if we're already connected to the device and services have been discovered. + /// This handles the case where navigation happens after services are already discovered. + /// + private void CheckExistingConnectionAndServices() + { + try + { + string currentConnectedAddress = _bleService.CurrentConnectedDeviceAddress; + + if (!string.IsNullOrEmpty(currentConnectedAddress) && currentConnectedAddress == _deviceAddress) + { + Tizen.Log.Info(Constants.LOG_TAG, $"Already connected to {_deviceAddress}. Checking for existing services."); + + // Get current services + var existingServices = _bleService.GetCurrentServices(); + + if (existingServices != null && existingServices.Any()) + { + Tizen.Log.Info(Constants.LOG_TAG, $"Found {existingServices.Count()} existing services for {_deviceAddress}."); + _uiContext.Post(_ => + { + ClearUuidList(); + _statusLabel.Text = $"Found {existingServices.Count()} services for {_deviceDisplayName}:"; + foreach (var uuid in existingServices) + { + AddUuidToList(uuid); + } + }, null); + } + else + { + Tizen.Log.Info(Constants.LOG_TAG, $"No existing services found for {_deviceAddress}. Waiting for service discovery."); + _uiContext.Post(_ => + { + _statusLabel.Text = $"Connected to {_deviceDisplayName}. Discovering services..."; + }, null); + } + } + else + { + Tizen.Log.Info(Constants.LOG_TAG, $"Not connected to {_deviceAddress} or connected to different device. Current: {currentConnectedAddress}"); + _uiContext.Post(_ => + { + _statusLabel.Text = $"Connecting to {_deviceDisplayName}..."; + }, null); + } + } + catch (Exception ex) + { + Tizen.Log.Error(Constants.LOG_TAG, $"Error checking existing connection: {ex.Message}"); + _uiContext.Post(_ => + { + _statusLabel.Text = $"Error checking connection status."; + }, null); + } + } + } +} diff --git a/Mobile/LeScanner/Lescanner/images/20251024_123329.jpg b/Mobile/LeScanner/Lescanner/images/20251024_123329.jpg new file mode 100644 index 000000000..89b797a22 Binary files /dev/null and b/Mobile/LeScanner/Lescanner/images/20251024_123329.jpg differ diff --git a/Mobile/LeScanner/Lescanner/images/20251024_123410.jpg b/Mobile/LeScanner/Lescanner/images/20251024_123410.jpg new file mode 100644 index 000000000..614664cf7 Binary files /dev/null and b/Mobile/LeScanner/Lescanner/images/20251024_123410.jpg differ diff --git a/Mobile/LeScanner/Lescanner/images/20251024_123424.jpg b/Mobile/LeScanner/Lescanner/images/20251024_123424.jpg new file mode 100644 index 000000000..5a3834ca1 Binary files /dev/null and b/Mobile/LeScanner/Lescanner/images/20251024_123424.jpg differ diff --git a/Mobile/LeScanner/Lescanner/images/20251024_123432.jpg b/Mobile/LeScanner/Lescanner/images/20251024_123432.jpg new file mode 100644 index 000000000..2724f342f Binary files /dev/null and b/Mobile/LeScanner/Lescanner/images/20251024_123432.jpg differ diff --git a/Mobile/LeScanner/Lescanner/images/20251024_123440.jpg b/Mobile/LeScanner/Lescanner/images/20251024_123440.jpg new file mode 100644 index 000000000..4e1b84679 Binary files /dev/null and b/Mobile/LeScanner/Lescanner/images/20251024_123440.jpg differ diff --git a/Mobile/LeScanner/Lescanner/shared/res/Lescanner.png b/Mobile/LeScanner/Lescanner/shared/res/Lescanner.png new file mode 100644 index 000000000..9f3cb9860 Binary files /dev/null and b/Mobile/LeScanner/Lescanner/shared/res/Lescanner.png differ diff --git a/Mobile/LeScanner/Lescanner/tizen-manifest.xml b/Mobile/LeScanner/Lescanner/tizen-manifest.xml new file mode 100644 index 000000000..c91aa1057 --- /dev/null +++ b/Mobile/LeScanner/Lescanner/tizen-manifest.xml @@ -0,0 +1,17 @@ + + + + + + Lescanner.png + + + + + + http://tizen.org/privilege/bluetooth + http://tizen.org/privilege/bluetooth.admin + + + + diff --git a/Mobile/LeScanner/Lescanner/tizen_dotnet_project.yaml b/Mobile/LeScanner/Lescanner/tizen_dotnet_project.yaml new file mode 100644 index 000000000..804608394 --- /dev/null +++ b/Mobile/LeScanner/Lescanner/tizen_dotnet_project.yaml @@ -0,0 +1,24 @@ +# csproj file path +csproj_file: Lescanner.csproj + +# Default profile, Tizen API version +profile: tizen +api_version: "9.0" + +# Build type [Debug/ Release/ Test] +build_type: Debug + +# Signing profile to be used for Tizen package signing +# If value is empty: "", active signing profile will be used +# Else If value is ".", default signing profile will be used +signing_profile: . + +# files monitored for dirty/modified status +files: + - Lescanner.csproj + - Lescanner.cs + - tizen-manifest.xml + - shared/res/Lescanner.png + +# project dependencies +deps: [] diff --git a/Mobile/LeScanner/README.md b/Mobile/LeScanner/README.md new file mode 100644 index 000000000..9dd011fc7 --- /dev/null +++ b/Mobile/LeScanner/README.md @@ -0,0 +1,17 @@ +# LeScanner +The bluetooth allows you to connect BT devices, and exchange data with the remote BT devices. + +![MainPage](./Screenshots/Tizen/MainPage.png) +![DeviceListPage](./Screenshots/Tizen/DeviceListPage.png) + + +### Verified Version +* Tizen.NET : 6.0.428 +* Tizen.NET.SDK : 10.0.111 + + +### Supported Profile +* Tizen 10.0 RPI4 + +### Author +* Harish Nanu J diff --git a/Mobile/LeScanner/Screenshots/Tizen/DeviceListPage.png b/Mobile/LeScanner/Screenshots/Tizen/DeviceListPage.png new file mode 100755 index 000000000..08b6f7494 Binary files /dev/null and b/Mobile/LeScanner/Screenshots/Tizen/DeviceListPage.png differ diff --git a/Mobile/LeScanner/Screenshots/Tizen/MainPage.png b/Mobile/LeScanner/Screenshots/Tizen/MainPage.png new file mode 100755 index 000000000..35cb20c0b Binary files /dev/null and b/Mobile/LeScanner/Screenshots/Tizen/MainPage.png differ diff --git a/Mobile/NetworkApp/NetworkApp.sln b/Mobile/NetworkApp/NetworkApp.sln new file mode 100644 index 000000000..0a66d299f --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp.sln @@ -0,0 +1,22 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31005.135 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetworkApp", "NetworkApp/NetworkApp.csproj", "{6c9320fb-191e-42f6-a843-d03457242819}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6c9320fb-191e-42f6-a843-d03457242819}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6c9320fb-191e-42f6-a843-d03457242819}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6c9320fb-191e-42f6-a843-d03457242819}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6c9320fb-191e-42f6-a843-d03457242819}.Release|Any CPU.Build.0 = Release|Any CPU + + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Mobile/NetworkApp/NetworkApp/APInfo.cs b/Mobile/NetworkApp/NetworkApp/APInfo.cs new file mode 100644 index 000000000..cac317afd --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/APInfo.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NetworkApp // Changed namespace to match the project +{ + /// + /// The Wi-Fi AP information + /// + public class APInfo + { + /// + /// Constructor + /// + /// ESSID of Wi-Fi AP + /// State of Wi-Fi AP + public APInfo(String Name, String State) + { + this.Name = Name; + this.State = State; + } + + /// + /// ESSID of Wi-Fi AP + /// + public String Name; + /// + /// State of Wi-Fi AP + /// + public String State; + } +} diff --git a/Mobile/NetworkApp/NetworkApp/AppStyles.cs b/Mobile/NetworkApp/NetworkApp/AppStyles.cs new file mode 100644 index 000000000..6a629ff27 --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/AppStyles.cs @@ -0,0 +1,40 @@ +using Tizen.NUI; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; + +namespace NetworkApp +{ + /// + /// Centralized class for defining UI raw style data for a modern, clean application. + /// + public static class AppStyles + { + // Color Palette - Refined for elegance + public static readonly Color PrimaryColor = new Color(0.2f, 0.5f, 0.9f, 1.0f); // A softer, more sophisticated blue + public static readonly Color PrimaryColorDark = new Color(0.15f, 0.4f, 0.75f, 1.0f); // Darker blue for hover/pressed states + public static readonly Color SecondaryColor = new Color(0.97f, 0.97f, 0.99f, 1.0f); // A very clean, soft off-white for cards/sections + public static readonly Color PageBackgroundColor = new Color(0.99f, 0.99f, 1.0f, 1.0f); // Barely off-white for page backgrounds + public static readonly Color TextColorPrimary = new Color(0.15f, 0.15f, 0.2f, 1.0f); // Softer black for primary text + public static readonly Color TextColorSecondary = new Color(0.55f, 0.55f, 0.6f, 1.0f); // Muted grey for secondary text + public static readonly Color AppBarTextColor = Color.White; + public static readonly Color ButtonTextColor = Color.White; + public static readonly Color ListBackgroundColor = Color.White; // White for list item backgrounds + public static readonly Color BorderColor = new Color(0.88f, 0.88f, 0.92f, 1.0f); // A very light, subtle border color + + // Typography - Reduced for better readability + public static readonly float TitlePointSize = 28.0f; // For main page titles + public static readonly float HeaderPointSize = 24.0f; // For section headers or list item titles + public static readonly float BodyPointSize = 20.0f; // For standard body text, labels + public static readonly float DetailPointSize = 18.0f; // For smaller details, status text + + // Spacing & Sizing + public static readonly Extents PagePadding = new Extents(24, 24, 24, 24); + public static readonly Extents ComponentPadding = new Extents(20, 20, 20, 20); + public static readonly Extents ListElementPadding = new Extents(16, 16, 16, 16); + public static readonly Extents ListElementMargin = new Extents(0, 0, 12, 0); // Vertical spacing between list items + public static readonly Size2D LayoutCellPadding = new Size2D(0, 20); // Vertical spacing in layouts + public static readonly Extents ButtonMargin = new Extents(0, 0, 16, 0); // Margin at the bottom of buttons + public static readonly float CornerRadius = 10.0f; // Slightly smaller for a more refined look + public static readonly float TextFieldBorderWidth = 1.5f; // For text fields (if using a View as a border) + } +} diff --git a/Mobile/NetworkApp/NetworkApp/ConnectionPage.cs b/Mobile/NetworkApp/NetworkApp/ConnectionPage.cs new file mode 100644 index 000000000..b96dd25ed --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/ConnectionPage.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using Tizen.NUI; +using Tizen.NUI.Components; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; // Required for DataTemplate and more advanced binding with CollectionView + +namespace NetworkApp +{ + /// + /// Tizen NUI ContentPage for Network Connection functionalities, + /// migrated from the Xamarin.Forms ConnectionPage. + /// + public class ConnectionPage : ContentPage + { + private TextLabel titleLabel; + + /// + /// Constructor for ConnectionPage. + /// + public ConnectionPage() + { + // AppBar (equivalent to Xamarin.Forms Title) + AppBar = new AppBar + { + Title = "Connection" // From ConnectionPage.Title = "Connection" + }; + + // Initialize UI components and layout + InitializeComponents(); + } + + /// + /// Initializes the UI components and layout for the page. + /// This method is equivalent to the Xamarin.Forms page's InitializeComponent. + /// + private void InitializeComponents() + { + var mainLayoutContainer = Resources.CreateMainLayoutContainer(); + + titleLabel = Resources.CreateTitleLabel("Connection Test"); + mainLayoutContainer.Add(titleLabel); + + var stringSourceList = new List + { + "Current Connection", "Wi-Fi State", "Cellular State", "IP Address", + "Wi-Fi MAC Address", "Proxy Address", "Profile List" + }; + + var listContentContainer = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = new Size2D(0, 12) // Spacing between list items + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + + foreach (var itemText in stringSourceList) + { + var itemContainer = Resources.CreateListItemContainer(); + itemContainer.Padding = AppStyles.ListElementPadding; + + var itemLabel = Resources.CreateHeaderLabel(itemText); + itemLabel.Name = itemText; + + itemContainer.Add(itemLabel); + itemContainer.TouchEvent += OnItemTapped; // Attach event to container + listContentContainer.Add(itemContainer); + } + + var scrollableList = Resources.CreateScrollableList(); + scrollableList.HeightSpecification = 700; + scrollableList.Add(listContentContainer); + mainLayoutContainer.Add(scrollableList); + + Content = mainLayoutContainer; + } + + /// + /// Handles the tap event on a list item container. + /// + /// The View container that was tapped. + /// Touch event arguments. + /// True if the event was consumed, false otherwise. + private bool OnItemTapped(object source, TouchEventArgs e) + { + // We only want to react to the touch start (or end) to avoid multiple actions. + // A simple check for TouchState.Down is often enough for a "tap" feel. + if (e.Touch.GetState(0) == PointStateType.Down) + { + var tappedContainer = source as View; + if (tappedContainer != null && tappedContainer.ChildCount > 0) + { + var tappedLabel = tappedContainer.GetChildAt(0) as TextLabel; + if (tappedLabel != null) + { + var selectedItemText = tappedLabel.Name; // Retrieve the text stored in the Name property + Tizen.Log.Info("ConnectionPage", $"Item tapped: {selectedItemText}"); + + ConnectionOperation operation = ConnectionOperation.CURRENT; // Default operation + + switch (selectedItemText) + { + case "Current Connection": + operation = ConnectionOperation.CURRENT; + break; + case "Wi-Fi State": + operation = ConnectionOperation.WIFISTATE; + break; + case "Cellular State": + operation = ConnectionOperation.CELLULARSTATE; + break; + case "IP Address": + operation = ConnectionOperation.IPADDRESS; + break; + case "Wi-Fi MAC Address": + operation = ConnectionOperation.WIFIMACADDRESS; + break; + case "Proxy Address": + operation = ConnectionOperation.PROXYADDRESS; + break; + case "Profile List": + operation = ConnectionOperation.PROFILELIST; + break; + } + + var navigator = NUIApplication.GetDefaultWindow().GetDefaultNavigator(); + navigator.Push(new ConnectionResultPage(operation)); + } + } + } + return true; // Event was consumed + } + } +} diff --git a/Mobile/NetworkApp/NetworkApp/ConnectionResultPage.cs b/Mobile/NetworkApp/NetworkApp/ConnectionResultPage.cs new file mode 100644 index 000000000..80373fa41 --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/ConnectionResultPage.cs @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Tizen.NUI; +using Tizen.NUI.Components; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; + +namespace NetworkApp +{ + /// + /// Enumeration for ConnectionOperation + /// + enum ConnectionOperation + { + CURRENT, + WIFISTATE, + CELLULARSTATE, + IPADDRESS, + WIFIMACADDRESS, + PROXYADDRESS, + PROFILELIST, + } + + /// + /// The ContentPage to show the result of operation tabbed on CollectionView of ConnectionPage + /// + class ConnectionResultPage : ContentPage + { + private TextLabel resultLabel; + private ScrollableBase profileScrollableList; // For ProfileList operation + private View profileListContentContainer; // Container for profile items + + private ConnectionOperation operation; + private TizenConnectionService connectionService; // Added TizenConnectionService field + + /// + /// Constructor + /// + /// ConnectionOperation + public ConnectionResultPage(ConnectionOperation op) + { + AppBar = new AppBar { Title = $"Connection Result: {op}" }; // Dynamic title based on operation + operation = op; + connectionService = new TizenConnectionService(); // Initialize TizenConnectionService + + var mainLayoutContainer = Resources.CreateMainLayoutContainer(); + // Use a slightly different padding for this page if needed, or default + Content = mainLayoutContainer; + + resultLabel = Resources.CreateBodyLabel(""); // Start with empty text + resultLabel.BackgroundColor = Color.Transparent; // Make background transparent + resultLabel.MultiLine = true; // Allow multiple lines + mainLayoutContainer.Add(resultLabel); + + profileListContentContainer = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = new Size2D(0, 8) // Spacing for profile items + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + + profileScrollableList = Resources.CreateScrollableList(); + profileScrollableList.HeightSpecification = 0; // Initially hidden + profileScrollableList.Add(profileListContentContainer); + mainLayoutContainer.Add(profileScrollableList); + + Operate(op); + } + + + /// + /// Event handler when a refresh button is clicked (if added later) + /// + /// Event sender + /// Event argument + public void OnClicked(object sender, EventArgs e) + { + Operate(operation); + } + + /// + /// Operate for each requested ConnectionOperation + /// + /// ConnectionOperation + public void Operate(ConnectionOperation op) + { + try + { + // Clear previous results + resultLabel.Text = ""; + profileScrollableList.HeightSpecification = 0; // Hide by setting height to 0 + // Clear previous profile items from the container + while (profileListContentContainer.ChildCount > 0) + { + profileListContentContainer.Remove(profileListContentContainer.GetChildAt(0)); + } + + switch (op) + { + // Show the current connected network + case ConnectionOperation.CURRENT: + CurrentConnection(); + break; + // Show the current Wi-Fi State + case ConnectionOperation.WIFISTATE: + WiFiState(); + break; + // Show the current Cellular state + case ConnectionOperation.CELLULARSTATE: + CellularState(); + break; + // Show the IPv4 address and the IPv6 address of the current connection + case ConnectionOperation.IPADDRESS: + IPAddress(); + break; + // Show the MAC address of Wi-Fi + case ConnectionOperation.WIFIMACADDRESS: + WiFiMACAddress(); + break; + // Show the proxy address + case ConnectionOperation.PROXYADDRESS: + ProxyAddress(); + break; + // Show the connection profiles + case ConnectionOperation.PROFILELIST: + // ProfileList is async, so it needs to be awaited + _ = ProfileList(); // Fire and forget, or properly await if UI needs to wait + break; + } + } + // C# API throws NotSupportedException if the API is not supported + catch (NotSupportedException) + { + resultLabel.Text = "The operation is not supported on this device"; + Tizen.Log.Error("Xaml2NUI", "ConnectionResultPage: NotSupportedException"); + } + catch (Exception ex) + { + resultLabel.Text = ex.ToString(); + Tizen.Log.Error("Xaml2NUI", $"ConnectionResultPage: Exception - {ex.Message}"); + } + } + + /// + /// Show the current connected network + /// + private void CurrentConnection() + { + try + { + string currentType = connectionService.CurrentType; + string currentState = connectionService.CurrentState; + // Check if the type or state indicates no connection + if (string.IsNullOrEmpty(currentType) || currentType.Equals("None", StringComparison.OrdinalIgnoreCase) || + currentState.Equals("Disconnected", StringComparison.OrdinalIgnoreCase)) + { + resultLabel.Text = "No active network connection."; + } + else + { + resultLabel.Text = $"Current Connection\nType: {currentType}\nState: {currentState}"; + } + Tizen.Log.Info("Xaml2NUI", "ConnectionResultPage: CurrentConnection retrieved."); + } + catch (Exception ex) + { + // Check for a more specific exception if the service throws one when not connected + // For now, a general exception is caught and assumed to mean 'not connected' or an error. + resultLabel.Text = "No active network connection or an error occurred."; + Tizen.Log.Error("Xaml2NUI", $"ConnectionResultPage: CurrentConnection error - {ex.ToString()}"); + } + } + + /// + /// Show the current Wi-Fi State + /// + private void WiFiState() + { + try + { + string wifiState = connectionService.WiFiState; + if (wifiState.Equals("Deactivated", StringComparison.OrdinalIgnoreCase) || + wifiState.Equals("Disconnected", StringComparison.OrdinalIgnoreCase) || + wifiState.Equals("Not Available", StringComparison.OrdinalIgnoreCase)) + { + resultLabel.Text = "Wi-Fi is not connected or active."; + } + else + { + resultLabel.Text = $"Wi-Fi State: {wifiState}"; + } + Tizen.Log.Info("Xaml2NUI", "ConnectionResultPage: WiFiState retrieved."); + } + catch (Exception ex) + { + resultLabel.Text = "Wi-Fi state is not available or an error occurred."; + Tizen.Log.Error("Xaml2NUI", $"ConnectionResultPage: WiFiState error - {ex.ToString()}"); + } + } + + /// + /// Show the current Cellular state + /// + private void CellularState() + { + try + { + string cellularState = connectionService.CellularState; + Tizen.Log.Info("Xaml2NUI", $"ConnectionResultPage: Raw cellularState value: '{cellularState}'"); // Log raw value + + // Trim whitespace for comparison + var trimmedCellularState = cellularState?.Trim(); + + if (string.IsNullOrEmpty(trimmedCellularState) || + trimmedCellularState.Equals("Error", StringComparison.OrdinalIgnoreCase) || // Specific check for "Error" string + trimmedCellularState.Equals("Deactivated", StringComparison.OrdinalIgnoreCase) || + trimmedCellularState.Equals("Disconnected", StringComparison.OrdinalIgnoreCase) || + trimmedCellularState.Equals("Not Available", StringComparison.OrdinalIgnoreCase) || + trimmedCellularState.Equals("OutOfService", StringComparison.OrdinalIgnoreCase)) // Common for cellular + { + resultLabel.Text = "Cellular is not connected or active."; + Tizen.Log.Info("Xaml2NUI", "ConnectionResultPage: Cellular state interpreted as 'not connected'."); + } + else + { + resultLabel.Text = $"Cellular State: {cellularState}"; // Display original, non-trimmed value if not an error state + Tizen.Log.Info("Xaml2NUI", "ConnectionResultPage: Cellular state displayed as is."); + } + } + catch (Exception ex) + { + // Log the detailed exception message to see if it contains "Error" + string exceptionMessage = ex.Message; + Tizen.Log.Error("Xaml2NUI", $"ConnectionResultPage: CellularState exception caught. Message: '{exceptionMessage}', StackTrace: {ex.ToString()}"); + + // Check if the generic exception message itself is what's being shown + if (exceptionMessage.Contains("Error", StringComparison.OrdinalIgnoreCase)) + { + resultLabel.Text = "Cellular is not connected or active (service error)."; + } + else + { + resultLabel.Text = "Cellular state is not available or an error occurred."; + } + } + } + + + /// + /// Show the IPv4 address and the IPv6 address of the current connection + /// + private void IPAddress() + { + try + { + string ipv4Address = connectionService.IPv4Address; + string ipv6Address = connectionService.IPv6Address; + resultLabel.Text = $"IPv4 Address: {ipv4Address}\nIPv6 Address: {ipv6Address}"; + Tizen.Log.Info("Xaml2NUI", "ConnectionResultPage: IPAddress retrieved."); + } + catch (Exception ex) + { + resultLabel.Text = $"Error getting IP address: {ex.Message}"; + Tizen.Log.Error("Xaml2NUI", $"ConnectionResultPage: IPAddress error - {ex.ToString()}"); + } + } + + /// + /// Show the MAC address of Wi-Fi + /// + private void WiFiMACAddress() + { + try + { + string wifiMACAddress = connectionService.WiFiMACAddress; + resultLabel.Text = $"Wi-Fi MAC Address: {wifiMACAddress}"; + Tizen.Log.Info("Xaml2NUI", "ConnectionResultPage: WiFiMACAddress retrieved."); + } + catch (Exception ex) + { + resultLabel.Text = $"Error getting Wi-Fi MAC address: {ex.Message}"; + Tizen.Log.Error("Xaml2NUI", $"ConnectionResultPage: WiFiMACAddress error - {ex.ToString()}"); + } + } + + /// + /// Show the proxy address + /// + private void ProxyAddress() + { + try + { + string proxyAddress = connectionService.ProxyAddress; + resultLabel.Text = $"Proxy Address: {proxyAddress}"; + Tizen.Log.Info("Xaml2NUI", "ConnectionResultPage: ProxyAddress retrieved."); + } + catch (Exception ex) + { + resultLabel.Text = $"Error getting proxy address: {ex.Message}"; + Tizen.Log.Error("Xaml2NUI", $"ConnectionResultPage: ProxyAddress error - {ex.ToString()}"); + } + } + + /// + /// Show the connection profiles + /// + private async Task ProfileList() + { + try + { + resultLabel.Text = "Getting profile list..."; + Tizen.Log.Info("Xaml2NUI", "ConnectionResultPage: ProfileList fetching."); + + List list = await connectionService.GetProfileListAsync(); + + // Update profileListView on the UI thread + var uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); + if (uiTaskScheduler != null) + { + await Task.Factory.StartNew(() => + { + if (list != null && list.Count > 0 && !(list.Count == 1 && list[0] == "Error")) + { + foreach (var profileName in list) + { + var profileItemContainer = Resources.CreateListItemContainer(); + profileItemContainer.Padding = AppStyles.ListElementPadding; + + var profileLabel = Resources.CreateBodyLabel(profileName); + + profileItemContainer.Add(profileLabel); + profileListContentContainer.Add(profileItemContainer); + } + profileScrollableList.HeightSpecification = LayoutParamPolicies.WrapContent; // Show the list + resultLabel.Text = $"Found {list.Count} profiles:"; // Update label text + } + else + { + resultLabel.Text = "No profiles found or error occurred."; + profileScrollableList.HeightSpecification = 0; // Ensure it's hidden + } + }, CancellationToken.None, TaskCreationOptions.None, uiTaskScheduler); + } + else + { + Tizen.Log.Warn("Xaml2NUI", "ProfileList: No SynchronizationContext found, UI update might be unsafe."); + if (list != null && list.Count > 0 && !(list.Count == 1 && list[0] == "Error")) + { + foreach (var profileName in list) + { + var profileItemContainer = Resources.CreateListItemContainer(); + profileItemContainer.Padding = AppStyles.ListElementPadding; + + var profileLabel = Resources.CreateBodyLabel(profileName); + + profileItemContainer.Add(profileLabel); + profileListContentContainer.Add(profileItemContainer); + } + profileScrollableList.HeightSpecification = LayoutParamPolicies.WrapContent; // Show the list + resultLabel.Text = $"Found {list.Count} profiles:"; // Update label text + } + else + { + resultLabel.Text = "No profiles found or error occurred."; + profileScrollableList.HeightSpecification = 0; // Ensure it's hidden + } + } + } + catch (Exception ex) + { + resultLabel.Text = $"Error getting profile list: {ex.Message}"; + Tizen.Log.Error("Xaml2NUI", $"ConnectionResultPage: ProfileList error - {ex.ToString()}"); + } + } + + } +} diff --git a/Mobile/NetworkApp/NetworkApp/Directory.Build.targets b/Mobile/NetworkApp/NetworkApp/Directory.Build.targets new file mode 100644 index 000000000..49b3ab641 --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/Directory.Build.targets @@ -0,0 +1,21 @@ + + + + + + + + $([System.IO.Path]::GetDirectoryName($(MSBuildProjectDirectory))) + + + + + + diff --git a/Mobile/NetworkApp/NetworkApp/MainMenuPage.cs b/Mobile/NetworkApp/NetworkApp/MainMenuPage.cs new file mode 100644 index 000000000..762f457b1 --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/MainMenuPage.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using Tizen.NUI; +using Tizen.NUI.Components; +using Tizen.NUI.BaseComponents; + +namespace NetworkApp +{ + /// + /// The main menu page for navigating to different network functionalities. + /// + class MainMenuPage : ContentPage + { + /// + /// Constructor + /// + public MainMenuPage() + { + AppBar = new AppBar { Title = "Network App Menu" }; + InitializeComponent(); + } + + /// + /// Initialize MainMenuPage. Add components and events. + /// + private void InitializeComponent() + { + var mainLayoutContainer = Resources.CreateMainLayoutContainer(); + // AppBar background is typically handled by the theme, but we can set it if needed. + // AppBar.BackgroundColor = AppStyles.PrimaryColor; + + var titleLabel = Resources.CreateTitleLabel("Select a Network Option"); + mainLayoutContainer.Add(titleLabel); + + var menuOptions = new List + { + "Connection", + "Wi-Fi", + "Wi-Fi Direct" + }; + + var listContentContainer = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = new Size2D(0, 12) // Spacing between menu items + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + + foreach (var optionText in menuOptions) + { + var itemContainer = Resources.CreateListItemContainer(); + // Customize list item for menu: use primary color for background to make it pop + itemContainer.BackgroundColor = AppStyles.PrimaryColor; + itemContainer.Padding = AppStyles.ListElementPadding; // Add padding inside the item + + var itemLabel = Resources.CreateHeaderLabel(optionText); // Use Header style for menu items + itemLabel.TextColor = AppStyles.AppBarTextColor; // Ensure text is white on blue background + itemLabel.HorizontalAlignment = HorizontalAlignment.Center; // Center text in menu items + itemLabel.Name = optionText; + + itemContainer.Add(itemLabel); + itemContainer.TouchEvent += OnMenuItemTapped; // Attach event to container + listContentContainer.Add(itemContainer); + } + + var scrollableList = Resources.CreateScrollableList(); + scrollableList.Add(listContentContainer); + mainLayoutContainer.Add(scrollableList); + + Content = mainLayoutContainer; + } + + /// + /// Handles the tap event on a menu item container. + /// + /// The View container that was tapped. + /// Touch event arguments. + /// True if the event was consumed, false otherwise. + private bool OnMenuItemTapped(object source, TouchEventArgs e) + { + if (e.Touch.GetState(0) == PointStateType.Down) + { + var tappedContainer = source as View; + if (tappedContainer != null && tappedContainer.ChildCount > 0) + { + var tappedLabel = tappedContainer.GetChildAt(0) as TextLabel; + if (tappedLabel != null) + { + var selectedItemText = tappedLabel.Name; + Tizen.Log.Info("MainMenuPage", $"Item tapped: {selectedItemText}"); + + var navigator = NUIApplication.GetDefaultWindow().GetDefaultNavigator(); + if (navigator != null) + { + switch (selectedItemText) + { + case "Connection": + navigator.Push(new ConnectionPage()); + break; + case "Wi-Fi": + navigator.Push(new WiFiPage()); + break; + case "Wi-Fi Direct": + navigator.Push(new WiFiDirectPage()); + break; + } + } + } + } + } + return true; // Event was consumed + } + } +} diff --git a/Mobile/NetworkApp/NetworkApp/NetworkApp.cs b/Mobile/NetworkApp/NetworkApp/NetworkApp.cs new file mode 100644 index 000000000..f0c25e767 --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/NetworkApp.cs @@ -0,0 +1,35 @@ +using System; +using Tizen.NUI; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; + +namespace NetworkApp +{ + class Program : NUIApplication + { + + private Window window; + protected override void OnCreate() + { + base.OnCreate(); + Initialize(); + } + + void Initialize() + { + window = NUIApplication.GetDefaultWindow(); + var navigator = window.GetDefaultNavigator(); ; + + // Create the initial page for the application + var initialPage = new MainMenuPage(); + navigator.Push(initialPage); + } + + static void Main(string[] args) + { + NUIApplication.IsUsingXaml = false; + var app = new Program(); + app.Run(args); + } + } +} diff --git a/Mobile/NetworkApp/NetworkApp/NetworkApp.csproj b/Mobile/NetworkApp/NetworkApp/NetworkApp.csproj new file mode 100644 index 000000000..066195631 --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/NetworkApp.csproj @@ -0,0 +1,22 @@ + + + + Exe + net6.0-tizen9.0 + + + + portable + + + None + + + + + + + + + + diff --git a/Mobile/NetworkApp/NetworkApp/Resources.cs b/Mobile/NetworkApp/NetworkApp/Resources.cs new file mode 100644 index 000000000..b75470232 --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/Resources.cs @@ -0,0 +1,222 @@ +using Tizen.NUI; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; + +namespace NetworkApp +{ + /// + /// Static helper class for creating pre-styled UI components using raw data from AppStyles. + /// This promotes consistency and reduces boilerplate code in page definitions. + /// + public static class Resources + { + /// + /// Creates a TextLabel styled as a main title. + /// + /// The text for the label. + /// A styled TextLabel. + public static TextLabel CreateTitleLabel(string text) + { + return new TextLabel + { + Text = text, + TextColor = AppStyles.TextColorPrimary, + PointSize = AppStyles.TitlePointSize, + HorizontalAlignment = HorizontalAlignment.Center, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + } + + /// + /// Creates a TextLabel styled for headers or list item titles. + /// + /// The text for the label. + /// A styled TextLabel. + public static TextLabel CreateHeaderLabel(string text) + { + return new TextLabel + { + Text = text, + TextColor = AppStyles.TextColorPrimary, + PointSize = AppStyles.HeaderPointSize, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + } + + /// + /// Creates a TextLabel styled for body text. + /// + /// The text for the label. + /// A styled TextLabel. + public static TextLabel CreateBodyLabel(string text) + { + return new TextLabel + { + Text = text, + TextColor = AppStyles.TextColorPrimary, + PointSize = AppStyles.BodyPointSize, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + } + + /// + /// Creates a TextLabel styled for detail or secondary text. + /// + /// The text for the label. + /// A styled TextLabel. + public static TextLabel CreateDetailLabel(string text) + { + return new TextLabel + { + Text = text, + TextColor = AppStyles.TextColorSecondary, + PointSize = AppStyles.DetailPointSize, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + } + + /// + /// Creates a Button styled as a primary action button. + /// + /// The text for the button. + /// A styled Button. + public static Button CreatePrimaryButton(string text) + { + var button = new Button + { + Text = text, + TextColor = AppStyles.ButtonTextColor, + PointSize = AppStyles.BodyPointSize, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = 72, // Fixed height for better touch experience + Margin = AppStyles.ButtonMargin, + BackgroundColor = AppStyles.PrimaryColor, + CornerRadius = AppStyles.CornerRadius, + }; + return button; + } + + /// + /// Creates a Button styled as a secondary action button. + /// + /// The text for the button. + /// A styled Button. + public static Button CreateSecondaryButton(string text) + { + var button = new Button + { + Text = text, + TextColor = AppStyles.PrimaryColor, + PointSize = AppStyles.BodyPointSize, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = 72, + Margin = AppStyles.ButtonMargin, + BackgroundColor = Color.Transparent, // Transparent background + CornerRadius = AppStyles.CornerRadius, + }; + // To create a border effect for a secondary button, we can wrap it in a View + // or use a background image. For simplicity, we'll keep it without a border for now. + return button; + } + + /// + /// Creates a View styled as a container for list items or cards. + /// + /// A styled View. + public static View CreateListItemContainer() + { + return new View + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + BackgroundColor = AppStyles.ListBackgroundColor, + CornerRadius = AppStyles.CornerRadius, + Margin = AppStyles.ListElementMargin, + // Padding = AppStyles.ListElementPadding // Padding will be handled by child elements or specific cases + }; + } + + /// + /// Creates a ScrollableBase styled for displaying lists. + /// + /// A styled ScrollableBase. + public static ScrollableBase CreateScrollableList() + { + return new ScrollableBase + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + ScrollingDirection = ScrollableBase.Direction.Vertical, + HideScrollbar = false, + BackgroundColor = Color.Transparent, + }; + } + + /// + /// Creates a main content View with a vertical linear layout. + /// + /// Optional padding to override the default page padding. + /// A styled View. + public static View CreateMainLayoutContainer(Extents padding = null) + { + var layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = AppStyles.LayoutCellPadding + }; + var view = new View + { + Layout = layout, + BackgroundColor = AppStyles.PageBackgroundColor, + Padding = padding ?? AppStyles.PagePadding + }; + view.WidthSpecification = LayoutParamPolicies.MatchParent; + view.HeightSpecification = LayoutParamPolicies.MatchParent; + return view; + } + + /// + /// Creates a TextField with a modern look, wrapped in a View to simulate a border. + /// + /// The placeholder text. + /// A View containing the styled TextField. + public static View CreateStyledTextField(string placeholderText) + { + var textField = new TextField + { + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = 64, // Fixed height + PlaceholderText = placeholderText, + BackgroundColor = Color.Transparent, // Transparent, border is handled by parent + TextColor = AppStyles.TextColorPrimary, + PointSize = AppStyles.BodyPointSize, + Padding = new Extents(16, 16, 16, 16), // Internal text padding + // Margin is handled by the container + }; + + var borderContainer = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + HorizontalAlignment = HorizontalAlignment.Begin, + VerticalAlignment = VerticalAlignment.Center + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = 64, // Match TextField height + BackgroundColor = AppStyles.ListBackgroundColor, // White background for the field + CornerRadius = AppStyles.CornerRadius, + Margin = new Extents(0, 0, 16, 0), // Bottom margin for the container + // To simulate a border, we can use a slightly larger background View with a different color + // or use a BorderImage if available. For simplicity, we'll rely on CornerRadius for now. + // A more robust border would require a custom drawing or a 9-patch image. + }; + borderContainer.Add(textField); + return borderContainer; + } + } +} diff --git a/Mobile/NetworkApp/NetworkApp/TizenConnectionService.cs b/Mobile/NetworkApp/NetworkApp/TizenConnectionService.cs new file mode 100644 index 000000000..61e8b628d --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/TizenConnectionService.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Tizen.Network.Connection; + +namespace NetworkApp +{ + /// + /// Tizen-specific Connection service implementation for the Xaml2NUI project. + /// This class directly uses Tizen.Network.Connection APIs. + /// + public class TizenConnectionService + { + private IEnumerable profileList; + + /// + /// Constructor + /// + public TizenConnectionService() + { + } + + /// + /// The type of the current profile for data connection (Disconnected, Wi-Fi, Cellular, etc.) + /// + public String CurrentType + { + get + { + try + { + // Call Tizen C# API + // Check if CurrentConnection is null, which can happen if disconnected + var currentConn = ConnectionManager.CurrentConnection; + if (currentConn == null) + { + return "Not Connected"; + } + return currentConn.Type.ToString(); + } + catch (System.InvalidOperationException ex) when (ex.Message.Contains("not initialized") || ex.Message.Contains("not available")) + { + Console.WriteLine($"[TizenConnectionService] CurrentType not available: {ex.Message}"); + return "Not Available"; + } + catch (Exception ex) + { + Console.WriteLine($"[TizenConnectionService] CurrentType get failed: {ex.Message}"); + return "Error"; // Fallback for unexpected errors + } + } + } + + /// + /// The state of the current profile for data connection + /// + public String CurrentState + { + get + { + try + { + // Call Tizen C# API + var currentConn = ConnectionManager.CurrentConnection; + if (currentConn == null) + { + return "Not Connected"; + } + return currentConn.State.ToString(); + } + catch (System.InvalidOperationException ex) when (ex.Message.Contains("not initialized") || ex.Message.Contains("not available")) + { + Console.WriteLine($"[TizenConnectionService] CurrentState not available: {ex.Message}"); + return "Not Available"; + } + catch (Exception ex) + { + Console.WriteLine($"[TizenConnectionService] CurrentState get failed: {ex.Message}"); + return "Error"; + } + } + } + + /// + /// The state of the Wi-Fi connection + /// + public String WiFiState + { + get + { + try + { + // Call Tizen C# API + return ConnectionManager.WiFiState.ToString(); + } + catch (System.InvalidOperationException ex) when (ex.Message.Contains("not initialized") || ex.Message.Contains("not available")) + { + Console.WriteLine($"[TizenConnectionService] WiFiState not available: {ex.Message}"); + return "Not Available"; + } + catch (Exception ex) + { + Console.WriteLine($"[TizenConnectionService] WiFiState get failed: {ex.Message}"); + return "Error"; + } + } + } + + /// + /// The state of the Cellular connection + /// + public String CellularState + { + get + { + try + { + // Call Tizen C# API + return ConnectionManager.CellularState.ToString(); + } + catch (Exception ex) // Catch any exception to prevent raw error from showing + { + Console.WriteLine($"[TizenConnectionService] CellularState get failed: {ex.Message}"); + // Check for common error messages to provide more specific feedback + if (ex.Message.Contains("not supported")) + { + return "Not Supported"; + } + if (ex.Message.Contains("not available") || ex.Message.Contains("not initialized")) + { + return "Not Available"; + } + return "Error"; // Generic fallback for other exceptions + } + } + } + + /// + /// The IPv4 address of the current connection + /// + public String IPv4Address + { + get + { + try + { + // Call Tizen C# API + var ipAddress = ConnectionManager.GetIPAddress(AddressFamily.IPv4); + return ipAddress?.ToString() ?? "N/A"; + } + catch (Exception ex) + { + Console.WriteLine($"[TizenConnectionService] IPv4Address get failed: {ex.Message}"); + return "Error"; + } + } + } + + /// + /// The IPv6 address of the current connection + /// + public String IPv6Address + { + get + { + try + { + // Call Tizen C# API + var ipAddress = ConnectionManager.GetIPAddress(AddressFamily.IPv6); + return ipAddress?.ToString() ?? "N/A"; + } + catch (Exception ex) + { + Console.WriteLine($"[TizenConnectionService] IPv6Address get failed: {ex.Message}"); + return "Error"; + } + } + } + + /// + /// The MAC address of the Wi-Fi + /// + public String WiFiMACAddress + { + get + { + try + { + // Call Tizen C# API + var macAddress = ConnectionManager.GetMacAddress(ConnectionType.WiFi); + return macAddress?.ToString() ?? "N/A"; + } + catch (Exception ex) + { + Console.WriteLine($"[TizenConnectionService] WiFiMACAddress get failed: {ex.Message}"); + return "Error"; + } + } + } + + /// + /// The proxy address of the current connection + /// + public String ProxyAddress + { + get + { + try + { + // Call Tizen C# API + var proxyAddress = ConnectionManager.GetProxy(AddressFamily.IPv4); + if (proxyAddress == null || string.IsNullOrEmpty(proxyAddress.ToString())) + { + return "No proxy configured"; + } + return proxyAddress.ToString(); + } + catch (Exception ex) + { + Console.WriteLine($"[TizenConnectionService] ProxyAddress get failed: {ex.Message}"); + // Check for common error messages + if (ex.Message.Contains("not supported")) + { + return "Not Supported"; + } + if (ex.Message.Contains("not available") || ex.Message.Contains("not initialized")) + { + return "Not Available"; + } + return "Error"; // Generic fallback + } + } + } + + /// + /// Get profile list as a list of profile name + /// + /// List of profile names + public async Task> GetProfileListAsync() + { + try + { + // Get profile list + await GetProfileListInternalAsync(); + List result = new List(); + if (profileList != null) + { + foreach (var item in profileList) + { + // Add name of connection profiles to a list + result.Add(item.Name); + } + } + return result; + } + catch (Exception ex) + { + Console.WriteLine($"[TizenConnectionService] GetProfileListAsync failed: {ex.Message}"); + return new List { "Error" }; + } + } + + /// + /// Gets the list of the profile internally + /// + private async Task GetProfileListInternalAsync() + { + // Call Tizen C# API + profileList = await ConnectionProfileManager.GetProfileListAsync(ProfileListType.Registered); + } + } +} diff --git a/Mobile/NetworkApp/NetworkApp/TizenWiFiDirectService.cs b/Mobile/NetworkApp/NetworkApp/TizenWiFiDirectService.cs new file mode 100644 index 000000000..5b8e0b725 --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/TizenWiFiDirectService.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Tizen.Network.WiFiDirect; + +namespace NetworkApp +{ + /// + /// Tizen-specific Wi-Fi Direct service implementation. + /// This class directly uses Tizen.Network.WiFiDirect APIs. + /// + public class TizenWiFiDirectService + { + private bool isScanning = false; + private List discoveredDevices = new List(); + public bool IsInitialized { get; private set; } = false; + + // Event to notify when a new device is discovered or the list is updated + public event EventHandler DevicesUpdated; + + public TizenWiFiDirectService() + { + try + { + Tizen.Log.Info("TizenWiFiDirectService", "Constructor called. Attempting to initialize."); + // Subscribe to Tizen's native events + WiFiDirectManager.DiscoveryStateChanged += OnDiscoveryStateChanged; + WiFiDirectManager.DeviceStateChanged += OnDeviceStateChanged; + IsInitialized = true; + Tizen.Log.Info("TizenWiFiDirectService", "Successfully initialized and subscribed to events."); + } + catch (Exception ex) + { + Tizen.Log.Error("TizenWiFiDirectService", $"Initialization failed: {ex.Message}. This might be due to Wi-Fi Direct not being supported or enabled on the device."); + IsInitialized = false; + // Ensure we are not subscribed if an exception occurred mid-subscription + try { WiFiDirectManager.DiscoveryStateChanged -= OnDiscoveryStateChanged; } catch { /* ignore */ } + try { WiFiDirectManager.DeviceStateChanged -= OnDeviceStateChanged; } catch { /* ignore */ } + } + } + + /// + /// Starts scanning for Wi-Fi Direct devices. + /// + /// True if scan initiation was successful, false otherwise. + public bool StartScan() + { + try + { + if (isScanning) + { + Tizen.Log.Info("TizenWiFiDirectService", "Scan already in progress."); + return true; + } + + Tizen.Log.Info("TizenWiFiDirectService", "StartScan called."); + // Clear previous results + discoveredDevices.Clear(); + + // Check if the Wi-Fi Direct is deactivated + if (WiFiDirectManager.State == WiFiDirectState.Deactivated) + { + Tizen.Log.Info("TizenWiFiDirectService", "Wi-Fi Direct is deactivated. Activating..."); + // Activation is asynchronous. Discovery will start in OnDeviceStateChanged. + WiFiDirectManager.Activate(); + // isScanning will be set to true once activation is complete and discovery starts. + } + else + { + // Already activated, start discovery directly + StartDiscoveryInternal(); + } + return true; + } + catch (Exception ex) + { + Tizen.Log.Error("TizenWiFiDirectService", $"StartScan failed: {ex.Message}"); + isScanning = false; + return false; + } + } + + /// + /// Stops scanning for Wi-Fi Direct devices. + /// + /// True if scan stop was successful, false otherwise. + public bool StopScan() + { + try + { + Tizen.Log.Info("TizenWiFiDirectService", "StopScan called."); + WiFiDirectManager.CancelDiscovery(); + // isScanning will be set to false in OnDiscoveryStateChanged + return true; + } + catch (Exception ex) + { + Tizen.Log.Error("TizenWiFiDirectService", $"StopScan failed: {ex.Message}"); + return false; + } + } + + /// + /// Gets the list of currently discovered devices. + /// + /// A list of WiFiDirectDevice objects. + public List GetDiscoveredDevices() + { + return new List(discoveredDevices); + } + + private void StartDiscoveryInternal() + { + Tizen.Log.Info("TizenWiFiDirectService", "StartDiscoveryInternal called."); + try + { + WiFiDirectManager.StartDiscovery(false, 0); // synchronous = false, channel = 0 (for all channels) + isScanning = true; // Set scanning state after initiating discovery + Tizen.Log.Info("TizenWiFiDirectService", "StartDiscovery called."); + } + catch (Exception ex) + { + Tizen.Log.Error("TizenWiFiDirectService", $"StartDiscoveryInternal failed: {ex.Message}"); + isScanning = false; + } + } + + private void OnDiscoveryStateChanged(object sender, DiscoveryStateChangedEventArgs e) + { + Tizen.Log.Info("TizenWiFiDirectService", $"OnDiscoveryStateChanged: {e.DiscoveryState}"); + if (e.DiscoveryState == WiFiDirectDiscoveryState.Found) + { + Tizen.Log.Info("TizenWiFiDirectService", "Discovery found peers."); + UpdateDiscoveredPeers(); + } + else + { + if (isScanning) + { + Tizen.Log.Info("TizenWiFiDirectService", $"Discovery state changed from 'Found' to '{e.DiscoveryState}'. Setting isScanning to false."); + isScanning = false; + } + } + } + + private void OnDeviceStateChanged(object sender, DeviceStateChangedEventArgs e) + { + Tizen.Log.Info("TizenWiFiDirectService", $"OnDeviceStateChanged: {e.DeviceState}"); + if (e.DeviceState == WiFiDirectDeviceState.Activated) + { + Tizen.Log.Info("TizenWiFiDirectService", "Wi-Fi Direct activated. Starting discovery."); + StartDiscoveryInternal(); + } + else if (e.DeviceState == WiFiDirectDeviceState.Deactivated) + { + isScanning = false; + discoveredDevices.Clear(); + DevicesUpdated?.Invoke(this, new WiFiDirectDeviceEventArgs(discoveredDevices)); + Tizen.Log.Info("TizenWiFiDirectService", "Wi-Fi Direct deactivated. Scan stopped and list cleared."); + } + } + + private void UpdateDiscoveredPeers() + { + try + { + IEnumerable peerList = WiFiDirectManager.GetDiscoveredPeers(); + if (peerList != null) + { + var newDeviceList = peerList.Select(p => new WiFiDirectDevice { Name = p.Name }).ToList(); + + if (!discoveredDevices.SequenceEqual(newDeviceList, new WiFiDirectDeviceComparer())) + { + discoveredDevices = newDeviceList; + Tizen.Log.Info("TizenWiFiDirectService", $"Discovered {discoveredDevices.Count} peers. Notifying UI."); + DevicesUpdated?.Invoke(this, new WiFiDirectDeviceEventArgs(discoveredDevices)); + } + else + { + Tizen.Log.Info("TizenWiFiDirectService", "Peer list has not changed."); + } + } + else + { + if (discoveredDevices.Any()) + { + discoveredDevices.Clear(); + DevicesUpdated?.Invoke(this, new WiFiDirectDeviceEventArgs(discoveredDevices)); + } + Tizen.Log.Info("TizenWiFiDirectService", "GetDiscoveredPeers returned null."); + } + } + catch (Exception ex) + { + Tizen.Log.Error("TizenWiFiDirectService", $"UpdateDiscoveredPeers failed: {ex.Message}"); + } + } + } + + // Custom EventArgs for passing a list of discovered devices + public class WiFiDirectDeviceEventArgs : EventArgs + { + public List Devices { get; } + + public WiFiDirectDeviceEventArgs(List devices) + { + Devices = devices; + } + } + + // Class representing a Wi-Fi Direct device for UI purposes. + // Maps relevant info from Tizen.Network.WiFiDirect.WiFiDirectPeer + public class WiFiDirectDevice + { + public string Name { get; set; } + + public override string ToString() + { + return Name; + } + } + + // Helper class to compare lists of WiFiDirectDevice + public class WiFiDirectDeviceComparer : IEqualityComparer + { + public bool Equals(WiFiDirectDevice x, WiFiDirectDevice y) + { + if (object.ReferenceEquals(x, y)) return true; + if (object.ReferenceEquals(x, null) || object.ReferenceEquals(y, null)) return false; + return x.Name == y.Name; + } + + public int GetHashCode(WiFiDirectDevice obj) + { + if (obj == null) return 0; + return obj.Name == null ? 0 : obj.Name.GetHashCode(); + } + } +} diff --git a/Mobile/NetworkApp/NetworkApp/TizenWiFiService.cs b/Mobile/NetworkApp/NetworkApp/TizenWiFiService.cs new file mode 100644 index 000000000..0784bbc8a --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/TizenWiFiService.cs @@ -0,0 +1,285 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Tizen.Network.WiFi; + +// Note: APInfo class should be available or defined in your project. +// It was part of the original Xamarin project (NetworkApp/APInfo.cs). +// If it's not in the Xaml2NUI project, you'll need to add it. +// For now, I'll assume it exists or will be added. + +namespace NetworkApp +{ + /// + /// Tizen-specific Wi-Fi service implementation for the Xaml2NUI project. + /// This class directly uses Tizen.Network.WiFi APIs. + /// + public class TizenWiFiService + { + private IEnumerable apList = null; + + /// + /// Constructor + /// + public TizenWiFiService() + { + apList = new List(); + // TODO: Consider if explicit Wi-Fi state change event handling is needed here, + // similar to what might have been implicitly managed by Xamarin's DependencyService. + // For example, WiFiManager.ConnectionStateChanged event, etc. + } + + /// + /// Call WiFiManager.ActivateAsync() to turn on Wi-Fi interface + /// + /// Task to do ActivateAsync + public async Task Activate() + { + Console.WriteLine($"[TizenWiFiService] Activate()"); + try + { + await WiFiManager.ActivateAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"[TizenWiFiService] Activate failed: {ex.Message}"); + // Consider re-throwing or handling as appropriate for the UI + throw; + } + } + + /// + /// Call WiFiManager.DeactivateAsync() to turn off Wi-Fi interface + /// + /// Task to do DeactivateAsync + public async Task Deactivate() + { + Console.WriteLine($"[TizenWiFiService] Deactivate()"); + try + { + await WiFiManager.DeactivateAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"[TizenWiFiService] Deactivate failed: {ex.Message}"); + throw; + } + } + + /// + /// Call WiFiManager.ScanAsync() to scan + /// + /// Task to do ScanAsync + public async Task Scan() + { + Console.WriteLine($"[TizenWiFiService] Scan()"); + try + { + await WiFiManager.ScanAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"[TizenWiFiService] Scan failed: {ex.Message}"); + throw; + } + } + + /// + /// Call WiFiManager.GetFoundAPs() to get scan result and return a list that contains Wi-Fi AP information + /// + /// scan result by a list of Wi-Fi AP information + public List ScanResult() + { + Console.WriteLine($"[TizenWiFiService] ScanResult()"); + try + { + apList = WiFiManager.GetFoundAPs(); + return GetAPList(); + } + catch (Exception ex) + { + Console.WriteLine($"[TizenWiFiService] ScanResult failed: {ex.Message}"); + return null; // Or return an empty list + } + } + + /// + /// Call WiFiAP.ConnectAsync() to connect the Wi-Fi AP + /// + /// ESSID of Wi-Fi AP to connect + /// password of Wi-Fi AP to connect + /// Task to do ConnectAsync + public async Task Connect(String essid, String password) + { + Console.WriteLine($"[TizenWiFiService] Connect() ESSID: {essid}, Password: {(string.IsNullOrEmpty(password) ? "N/A" : "****")}"); + WiFiAP ap = FindAP(essid); + if (ap == null) + { + var ex = new ArgumentException($"Cannot find AP with ESSID: {essid}"); + Console.WriteLine($"[TizenWiFiService] Connect failed: {ex.Message}"); + throw ex; + } + + if (password.Length > 0) + { + // The Tizen API may have changed; KeyManagement property is no longer available. + // SetPassphrase should handle security type internally or ignore password for open networks. + ap.SecurityInformation.SetPassphrase(password); + } + try + { + await ap.ConnectAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"[TizenWiFiService] Connect to {essid} failed: {ex.Message}"); + throw; + } + } + + /// + /// Call WiFiAP.DisconnectAsync() to disconnect the Wi-Fi AP + /// + /// ESSID of Wi-Fi AP to disconnect + /// Task to do DisconnectAsync + public async Task Disconnect(String essid) + { + Console.WriteLine($"[TizenWiFiService] Disconnect() ESSID: {essid}"); + WiFiAP ap = FindAP(essid); + if (ap == null) + { + var ex = new ArgumentException($"Cannot find AP with ESSID: {essid} to disconnect."); + Console.WriteLine($"[TizenWiFiService] Disconnect failed: {ex.Message}"); + throw ex; + } + try + { + await ap.DisconnectAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"[TizenWiFiService] Disconnect from {essid} failed: {ex.Message}"); + throw; + } + } + + /// + /// Call WiFiAP.ForgetAP() to forget the Wi-Fi AP + /// + /// ESSID of Wi-Fi AP to forget + public void Forget(String essid) + { + Console.WriteLine($"[TizenWiFiService] Forget() ESSID: {essid}"); + WiFiAP ap = FindAP(essid); + if (ap == null) + { + Console.WriteLine($"[TizenWiFiService] Forget failed: Can't find AP {essid}"); + return; + } + try + { + ap.ForgetAP(); + } + catch (Exception ex) + { + Console.WriteLine($"[TizenWiFiService] Forget {essid} failed: {ex.Message}"); + throw; + } + } + + /// + /// Find a WiFiAP instance + /// + /// ESSID of Wi-Fi AP to find from apList + /// WiFiAP instance with the ESSID + private WiFiAP FindAP(String essid) + { + Console.WriteLine($"[TizenWiFiService] FindAP() ESSID: {essid}"); + // Ensure apList is populated if it's empty or stale. + // The original implementation called ScanResult() here, which might be too slow + // if called frequently (e.g., for connect/disconnect). + // Consider if apList should be refreshed by a Scan call initiated by the UI. + // For now, mirroring original behavior: + if (apList == null || !apList.GetEnumerator().MoveNext()) // Basic check if list is empty + { + ScanResult(); // This will update apList + } + + foreach (var item in apList) + { + // Ensure NetworkInformation and Essid are not null + if (item?.NetworkInformation?.Essid != null) + { + Console.WriteLine($"[TizenWiFiService] FindAP() Checking AP\t{item.NetworkInformation.Essid}"); + if (item.NetworkInformation.Essid.Equals(essid)) + { + return item; + } + } + } + Console.WriteLine($"[TizenWiFiService] FindAP() AP with ESSID {essid} not found in current list."); + return null; + } + + /// + /// Check if Wi-Fi is powered on + /// + /// True if Wi-Fi is on. False, otherwise + public bool IsActive() + { + Console.WriteLine($"[TizenWiFiService] IsActive()"); + try + { + return WiFiManager.IsActive; + } + catch (Exception ex) + { + Console.WriteLine($"[TizenWiFiService] IsActive check failed: {ex.Message}"); + return false; // Default to false on error + } + } + + /// + /// Get the ESSID of the AP that this device is connected to + /// + /// ESSID of the connected Wi-Fi AP + public String ConnectedAP() + { + Console.WriteLine($"[TizenWiFiService] ConnectedAP()"); + try + { + WiFiAP connectedAp = WiFiManager.GetConnectedAP(); + return connectedAp?.NetworkInformation?.Essid; // Use null conditional for safety + } + catch (Exception ex) + { + // This can throw if no AP is connected, depending on Tizen API version/behavior + Console.WriteLine($"[TizenWiFiService] ConnectedAP failed: {ex.Message}"); + return null; // Or string.Empty + } + } + + /// + /// Get a list of scanned Wi-Fi APs + /// + /// List of Wi-Fi AP information + public List GetAPList() + { + List apInfoList = new List(); + if (apList == null) + { + Console.WriteLine("[TizenWiFiService] GetAPList() apList is null."); + return apInfoList; // Return empty list + } + + foreach (var item in apList) + { + if (item?.NetworkInformation != null) + { + apInfoList.Add(new APInfo(item.NetworkInformation.Essid, item.NetworkInformation.ConnectionState.ToString())); + } + } + return apInfoList; + } + } +} diff --git a/Mobile/NetworkApp/NetworkApp/WiFiDirectPage.cs b/Mobile/NetworkApp/NetworkApp/WiFiDirectPage.cs new file mode 100644 index 000000000..af2cc2b92 --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/WiFiDirectPage.cs @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using Tizen.NUI; +using Tizen.NUI.Components; +using Tizen.NUI.BaseComponents; + +namespace NetworkApp +{ + /// + /// The ContentPage for testing Tizen.Network.WiFiDirect C# API + /// + class WiFiDirectPage : ContentPage + { + private Button scanButton; + private ScrollableBase scanScrollableList; + private View scanListContentContainer; + private TextLabel resultLabel; + + private TizenWiFiDirectService wifiDirectService; + + /// + /// Constructor + /// + public WiFiDirectPage() + { + AppBar = new AppBar { Title = "WiFiDirect" }; + InitializeComponent(); + } + + /// + /// Initialize WiFiDirectPage. Add components and events. + /// + private void InitializeComponent() + { + wifiDirectService = new TizenWiFiDirectService(); + + var mainLayoutContainer = Resources.CreateMainLayoutContainer(); + // WiFiDirectPage can use the default light theme from AppStyles + + var titleLabel = Resources.CreateTitleLabel("Wi-Fi Direct Test"); + mainLayoutContainer.Add(titleLabel); + + scanButton = Resources.CreatePrimaryButton("Start Scan"); + scanButton.Clicked += OnClicked; + mainLayoutContainer.Add(scanButton); + + if (!wifiDirectService.IsInitialized) + { + Tizen.Log.Error("Xaml2NUI", "WiFiDirectPage: TizenWiFiDirectService failed to initialize."); + // resultLabel will be created below, set text after creation + scanButton.IsEnabled = false; + } + else + { + wifiDirectService.DevicesUpdated += OnDevicesUpdated; + Tizen.Log.Info("Xaml2NUI", "WiFiDirectPage: TizenWiFiDirectService initialized successfully."); + } + + scanListContentContainer = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = new Size2D(0, 8) // Less vertical padding for device list items + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + + scanScrollableList = Resources.CreateScrollableList(); + scanScrollableList.Add(scanListContentContainer); + mainLayoutContainer.Add(scanScrollableList); + + resultLabel = Resources.CreateDetailLabel(""); // Start with empty text + resultLabel.BackgroundColor = Color.Transparent; // Make background transparent + resultLabel.HorizontalAlignment = HorizontalAlignment.Center; // Center status text + if (!wifiDirectService.IsInitialized) + { + resultLabel.Text = "Wi-Fi Direct service could not be initialized. It may not be supported or enabled on this device."; + } + mainLayoutContainer.Add(resultLabel); + + Content = mainLayoutContainer; + } + + /// + /// Event handler when button is clicked + /// + /// Event sender + /// Event argument + private void OnClicked(object sender, EventArgs e) + { + try + { + if (scanButton.Text.Equals("Start Scan")) + { + if (wifiDirectService.StartScan()) + { + scanButton.Text = "Stop Scanning"; + resultLabel.Text = "Scanning for devices..."; + // ClearDeviceList is no longer needed as UpdateDeviceList handles clearing. + // Explicitly clear by sending an empty list if desired, or rely on first DevicesUpdated event. + // For now, we rely on the first DevicesUpdated event to clear/update the list. + UpdateDeviceList(new List()); // Clear list immediately + } + else + { + resultLabel.Text = "Failed to start scan."; + } + } + else // "Stop Scanning" + { + if (wifiDirectService.StopScan()) + { + scanButton.Text = "Start Scan"; + resultLabel.Text = "Scan stopped."; + } + else + { + resultLabel.Text = "Failed to stop scan."; + } + } + } + catch (Exception ex) + { + resultLabel.Text = $"Error: {ex.Message}"; + Tizen.Log.Error("Xaml2NUI", $"WiFiDirectPage: OnClicked Exception - {ex.ToString()}"); + } + } + + /// + /// Event handler when the list of discovered devices is updated via TizenWiFiDirectService + /// + /// Event sender (TizenWiFiDirectService) + /// Event argument containing the list of discovered devices + private void OnDevicesUpdated(object sender, WiFiDirectDeviceEventArgs e) + { + Tizen.Log.Info("Xaml2NUI", $"WiFiDirectPage: Devices updated. Count: {e.Devices.Count}"); + UpdateDeviceList(e.Devices); + } + + private void UpdateDeviceList(List devices) + { + // Clear existing items from the UI list + while (scanListContentContainer.ChildCount > 0) + { + scanListContentContainer.Remove(scanListContentContainer.GetChildAt(0)); + } + + if (devices != null && devices.Count > 0) + { + foreach (var device in devices) + { + var deviceContainer = Resources.CreateListItemContainer(); + deviceContainer.Padding = AppStyles.ListElementPadding; + + var deviceLabel = Resources.CreateBodyLabel(device.ToString()); + + deviceContainer.Add(deviceLabel); + scanListContentContainer.Add(deviceContainer); + } + scanScrollableList.HeightSpecification = LayoutParamPolicies.WrapContent; // Show the list + resultLabel.Text = $"Found {devices.Count} device(s)."; // Update status + } + else + { + scanScrollableList.HeightSpecification = 0; // Hide the list if no devices + if (!resultLabel.Text.Equals("Scanning for devices...")) + { + resultLabel.Text = "No devices found."; + } + } + } + } +} diff --git a/Mobile/NetworkApp/NetworkApp/WiFiPage.cs b/Mobile/NetworkApp/NetworkApp/WiFiPage.cs new file mode 100644 index 000000000..d103c7ede --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/WiFiPage.cs @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using Tizen.NUI; +using Tizen.NUI.Components; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Binding; + +namespace NetworkApp +{ + /// + /// The ContentPage for testing Tizen.Network.WiFi C# API + /// + class WiFiPage : ContentPage + { + // ILog log = DependencyService.Get(); // To be handled if a similar service is available in Tizen NUI + + private TextLabel titleLabel; + + /// + /// Constructor + /// + public WiFiPage() + { + AppBar = new AppBar { Title = "Wi-Fi" }; + InitializeComponent(); + } + + /// + /// Initialize WiFiPage. Add components and events. + /// + private void InitializeComponent() + { + var mainLayoutContainer = Resources.CreateMainLayoutContainer(); + + titleLabel = Resources.CreateTitleLabel("Wi-Fi Test"); + mainLayoutContainer.Add(titleLabel); + + var wifiOperationsList = new List + { + "Activate", + "Deactivate", + "Scan", + "Connect", + "Disconnect", + "Forget", + }; + + var listContentContainer = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = new Size2D(0, 12) // Spacing between list items + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + + foreach (var opText in wifiOperationsList) + { + var itemContainer = Resources.CreateListItemContainer(); + itemContainer.Padding = AppStyles.ListElementPadding; + + var itemLabel = Resources.CreateHeaderLabel(opText); + itemLabel.Name = opText; // Store operation text for identification + + itemContainer.Add(itemLabel); + // Attach the TouchEvent to the container to ensure the whole item is tappable + itemContainer.TouchEvent += OnWiFiItemTapped; + listContentContainer.Add(itemContainer); + } + + var scrollableList = Resources.CreateScrollableList(); + // Give scrollable list a more defined height to make it scrollable if content is long + scrollableList.HeightSpecification = 700; + scrollableList.Add(listContentContainer); + mainLayoutContainer.Add(scrollableList); + + Content = mainLayoutContainer; + } + + /// + /// Handles the tap event on a Wi-Fi operation item container. + /// + /// The View container that was tapped. + /// Touch event arguments. + /// True if the event was consumed, false otherwise. + private bool OnWiFiItemTapped(object source, TouchEventArgs e) + { + if (e.Touch.GetState(0) == PointStateType.Down) + { + var tappedContainer = source as View; + if (tappedContainer != null && tappedContainer.ChildCount > 0) + { + // We assume the first child is the TextLabel holding the operation name + var tappedLabel = tappedContainer.GetChildAt(0) as TextLabel; + if (tappedLabel != null) + { + var selectedItemText = tappedLabel.Name; + Tizen.Log.Info("WiFiPage", $"Item tapped: {selectedItemText}"); + + WiFiResultPage.WiFiOperation operation = WiFiResultPage.WiFiOperation.Activate; // Default + + switch (selectedItemText) + { + case "Activate": + operation = WiFiResultPage.WiFiOperation.Activate; + break; + case "Deactivate": + operation = WiFiResultPage.WiFiOperation.Deactivate; + break; + case "Scan": + operation = WiFiResultPage.WiFiOperation.Scan; + break; + case "Connect": + operation = WiFiResultPage.WiFiOperation.Connect; + break; + case "Disconnect": + operation = WiFiResultPage.WiFiOperation.Disconnect; + break; + case "Forget": + operation = WiFiResultPage.WiFiOperation.Forget; + break; + } + + var navigator = NUIApplication.GetDefaultWindow().GetDefaultNavigator(); + navigator.Push(new WiFiResultPage(operation)); + } + } + } + return true; // Event was consumed + } + } +} diff --git a/Mobile/NetworkApp/NetworkApp/WiFiResultPage.cs b/Mobile/NetworkApp/NetworkApp/WiFiResultPage.cs new file mode 100644 index 000000000..21562753d --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/WiFiResultPage.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; // Added for Task operations +using Tizen.NUI; +using Tizen.NUI.BaseComponents; +using Tizen.NUI.Components; + +namespace NetworkApp // Added namespace declaration +{ + public class WiFiResultPage : ContentPage + { + public enum WiFiOperation + { + Activate, + Deactivate, + Scan, + Connect, + Disconnect, + Forget + } + + private WiFiOperation currentOperation; + private TizenWiFiService wifiService; // Added TizenWiFiService field + + // UI Elements + private TextLabel operationLabel; + private ScrollableBase resultScrollableList; // For scan results + private View resultListContentContainer; // Container for scan result items + private Button actionButton; + private TextLabel statusLabel; + private View ssidEntryContainer; // For Connect operation + private View passwordEntryContainer; // For Connect operation + + public WiFiResultPage(WiFiOperation operation) + { + currentOperation = operation; + wifiService = new TizenWiFiService(); // Initialize TizenWiFiService + AppBar = new AppBar { Title = $"WiFi {operation} Result" }; // Set AppBar title + + InitializeComponents(); + SetupUIBasedOnOperation(); + } + + private void InitializeComponents() + { + var mainLayoutContainer = Resources.CreateMainLayoutContainer(); + Content = mainLayoutContainer; + + operationLabel = Resources.CreateHeaderLabel(""); // Text will be set in SetupUIBasedOnOperation + operationLabel.Margin = new Extents(0, 0, 16, 0); // Add some bottom margin + + resultListContentContainer = new View + { + Layout = new LinearLayout + { + LinearOrientation = LinearLayout.Orientation.Vertical, + CellPadding = new Size2D(0, 8) // Spacing for scan result items + }, + WidthSpecification = LayoutParamPolicies.MatchParent, + HeightSpecification = LayoutParamPolicies.WrapContent, + }; + + resultScrollableList = Resources.CreateScrollableList(); + resultScrollableList.Margin = new Extents(0, 0, 16, 0); // Add some bottom margin + resultScrollableList.Add(resultListContentContainer); + + actionButton = Resources.CreatePrimaryButton("Action"); // Text will be set in SetupUIBasedOnOperation + actionButton.Clicked += OnActionButtonClicked; + // Margin is already handled by AppStyles.PrimaryButtonStyle + + statusLabel = Resources.CreateDetailLabel("Status: Idle"); + statusLabel.BackgroundColor = Color.Transparent; // Make background transparent + statusLabel.Margin = new Extents(0,0,0,0); // Override default margin from style if needed + + ssidEntryContainer = Resources.CreateStyledTextField("Enter SSID"); // Store the container + passwordEntryContainer = Resources.CreateStyledTextField("Enter Password"); // Store the container + } + + private void SetupUIBasedOnOperation() + { + // Clear previous content from the main container before adding new elements + if (Content is View mainLayoutContainer) + { + while (mainLayoutContainer.ChildCount > 0) + { + mainLayoutContainer.Remove(mainLayoutContainer.GetChildAt(0)); + } + mainLayoutContainer.Add(operationLabel); + } + else + { + // Fallback or error handling if Content is not a View as expected + Tizen.Log.Error("WiFiResultPage", "Content is not a View, cannot setup UI."); + return; + } + + switch (currentOperation) + { + case WiFiOperation.Activate: + operationLabel.Text = "WiFi Activation"; + actionButton.Text = "Activate WiFi"; + mainLayoutContainer.Add(actionButton); + mainLayoutContainer.Add(statusLabel); + break; + + case WiFiOperation.Deactivate: + operationLabel.Text = "WiFi Deactivation"; + actionButton.Text = "Deactivate WiFi"; + mainLayoutContainer.Add(actionButton); + mainLayoutContainer.Add(statusLabel); + break; + + case WiFiOperation.Scan: + operationLabel.Text = "WiFi Scan"; + actionButton.Text = "Scan Networks"; + mainLayoutContainer.Add(actionButton); + mainLayoutContainer.Add(resultScrollableList); // ScrollableBase for scan results + mainLayoutContainer.Add(statusLabel); + break; + + case WiFiOperation.Connect: + operationLabel.Text = "WiFi Connection"; + mainLayoutContainer.Add(ssidEntryContainer); + mainLayoutContainer.Add(passwordEntryContainer); + actionButton.Text = "Connect"; + mainLayoutContainer.Add(actionButton); + mainLayoutContainer.Add(statusLabel); + break; + + case WiFiOperation.Disconnect: + operationLabel.Text = "WiFi Disconnection"; + actionButton.Text = "Disconnect Current"; + mainLayoutContainer.Add(actionButton); + mainLayoutContainer.Add(statusLabel); + break; + + case WiFiOperation.Forget: + operationLabel.Text = "Forget Network"; + mainLayoutContainer.Add(ssidEntryContainer); // SSID to forget + actionButton.Text = "Forget Network"; + mainLayoutContainer.Add(actionButton); + mainLayoutContainer.Add(statusLabel); + break; + } + } + + /// + /// Helper method to check if Wi-Fi is currently active. + /// + /// True if Wi-Fi is active, false otherwise. + private async Task IsWiFiActiveAsync() + { + // This is a placeholder. You'll need to replace this with the actual call + // to your TizenWiFiService to get the current Wi-Fi state. + // For example, if TizenWiFiService has a boolean property 'IsActive': + // return wifiService.IsActive; + + // Or if it has a method that returns state: + // var state = await wifiService.GetStateAsync(); + // return state == WiFiState.Enabled; // Assuming WiFiState is an enum + + // For demonstration, let's assume a property 'IsActive' exists on TizenWiFiService. + // If the property is synchronous, you can access it directly. + // If it's async, use await. + // This example assumes a synchronous property for simplicity. + try + { + // Replace 'wifiService.IsActive' with the actual property/method from your service. + // If your service uses an enum for state, it might look like: + // return wifiService.CurrentState == Tizen.Network.WiFi.WiFiState.Enabled; + if (wifiService != null) + { + // This is a common pattern. Adjust if your service is different. + // For instance, if there's a 'GetState()' method: + // var currentState = await wifiService.GetStateAsync(); // If async + // var currentState = wifiService.GetState(); // If sync + // return currentState == YourWiFiStateEnum.Enabled; + + // Assuming a boolean method 'IsActive()' for now: + return wifiService.IsActive(); + } + } + catch (Exception ex) + { + Tizen.Log.Error("WiFiResultPage", $"Error checking Wi-Fi state: {ex.Message}"); + } + return false; // Default to false if service is null or error occurs + } + + // OnResultListItemSelected was for CollectionView. + // If manual selection/tap on scan results is needed, a TouchEvent handler + // would need to be added to each TextLabel in the scan results list. + + private void OnActionButtonClicked(object sender, ClickedEventArgs args) + { + Tizen.Log.Info("WiFiResultPage", $"Action button clicked for: {currentOperation}"); + statusLabel.Text = $"Performing {currentOperation}..."; + + // Placeholder for actual async operations + // In a real app, these would call IWiFi service methods + // and update UI on completion. + PerformOperationAsync(currentOperation); + } + + private async void PerformOperationAsync(WiFiOperation operation) + { + try + { + switch (operation) + { + case WiFiOperation.Activate: + // Check if WiFi is already active + if (await IsWiFiActiveAsync()) + { + statusLabel.Text = "WiFi is already active."; + } + else + { + statusLabel.Text = "Activating WiFi..."; + await wifiService.Activate(); + // Optionally, check again if activation was successful + if (await IsWiFiActiveAsync()) + { + statusLabel.Text = "WiFi Activation Successful."; + } + else + { + statusLabel.Text = "WiFi Activation failed. Please try again."; + } + } + break; + + case WiFiOperation.Deactivate: + // Check if WiFi is already inactive + if (!await IsWiFiActiveAsync()) + { + statusLabel.Text = "WiFi is already inactive."; + } + else + { + statusLabel.Text = "Deactivating WiFi..."; + await wifiService.Deactivate(); + // Optionally, check again if deactivation was successful + if (!await IsWiFiActiveAsync()) + { + statusLabel.Text = "WiFi Deactivation Successful."; + } + else + { + statusLabel.Text = "WiFi Deactivation failed. Please try again."; + } + } + break; + + case WiFiOperation.Scan: + statusLabel.Text = "Scanning for networks..."; + await wifiService.Scan(); + // Allow a brief moment for scan results to be populated internally by Tizen + await Task.Delay(500); // Short delay before fetching results + var apInfoList = wifiService.ScanResult(); // ScanResult() is synchronous + + // Clear previous scan results from the container + while (resultListContentContainer.ChildCount > 0) + { + resultListContentContainer.Remove(resultListContentContainer.GetChildAt(0)); + } + + if (apInfoList != null) + { + int count = 0; + foreach (var apInfo in apInfoList) + { + var networkItemContainer = Resources.CreateListItemContainer(); + networkItemContainer.Padding = AppStyles.ListElementPadding; + + var networkLabel = Resources.CreateBodyLabel($"{apInfo.Name} ({apInfo.State})"); + + networkItemContainer.Add(networkLabel); + resultListContentContainer.Add(networkItemContainer); + count++; + } + resultScrollableList.HeightSpecification = LayoutParamPolicies.WrapContent; // Show the list + statusLabel.Text = $"Scan complete. Found {count} networks:"; + } + else + { + resultScrollableList.HeightSpecification = 0; // Hide if no results + statusLabel.Text = "Scan failed or no networks found."; + } + break; + + case WiFiOperation.Connect: + var ssidField = ssidEntryContainer.GetChildAt(0) as TextField; + var passwordField = passwordEntryContainer.GetChildAt(0) as TextField; + if (ssidField != null && !string.IsNullOrEmpty(ssidField.Text)) + { + statusLabel.Text = $"Connecting to {ssidField.Text}..."; + await wifiService.Connect(ssidField.Text, passwordField?.Text); + statusLabel.Text = $"Successfully connected to {ssidField.Text}."; + } + else + { + statusLabel.Text = "Connection Failed: SSID cannot be empty."; + } + break; + + case WiFiOperation.Disconnect: + string connectedAp = wifiService.ConnectedAP(); + if (!string.IsNullOrEmpty(connectedAp)) + { + statusLabel.Text = $"Disconnecting from {connectedAp}..."; + await wifiService.Disconnect(connectedAp); + statusLabel.Text = $"Successfully disconnected from {connectedAp}."; + } + else + { + statusLabel.Text = "Disconnection Failed: Not connected to any AP."; + } + break; + + case WiFiOperation.Forget: + var forgetSsidField = ssidEntryContainer.GetChildAt(0) as TextField; + if (forgetSsidField != null && !string.IsNullOrEmpty(forgetSsidField.Text)) + { + statusLabel.Text = $"Forgetting {forgetSsidField.Text}..."; + wifiService.Forget(forgetSsidField.Text); // Forget is synchronous + statusLabel.Text = $"Successfully forgot {forgetSsidField.Text}."; + } + else + { + statusLabel.Text = "Forget Failed: SSID cannot be empty."; + } + break; + } + } + catch (Exception ex) + { + // Update UI on the main thread in case of an error + // NUI operations are generally thread-safe for property updates like Text, + // but for more complex UI changes, ensure it's on the main thread. + // For simplicity, direct update here. + statusLabel.Text = $"{operation} Failed: {ex.Message}"; + Tizen.Log.Error("WiFiResultPage", $"Error in PerformOperationAsync for {operation}: {ex.ToString()}"); + } +} +} +} diff --git a/Mobile/NetworkApp/NetworkApp/shared/res/NetworkApp.png b/Mobile/NetworkApp/NetworkApp/shared/res/NetworkApp.png new file mode 100644 index 000000000..9f3cb9860 Binary files /dev/null and b/Mobile/NetworkApp/NetworkApp/shared/res/NetworkApp.png differ diff --git a/Mobile/NetworkApp/NetworkApp/tizen-manifest.xml b/Mobile/NetworkApp/NetworkApp/tizen-manifest.xml new file mode 100644 index 000000000..3d8530da8 --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/tizen-manifest.xml @@ -0,0 +1,19 @@ + + + + + + NetworkApp.png + + + + + + http://tizen.org/privilege/network.get + http://tizen.org/privilege/network.set + http://tizen.org/privilege/network.profile + http://tizen.org/privilege/wifidirect + + + + diff --git a/Mobile/NetworkApp/NetworkApp/tizen_dotnet_project.yaml b/Mobile/NetworkApp/NetworkApp/tizen_dotnet_project.yaml new file mode 100644 index 000000000..0f5fbae14 --- /dev/null +++ b/Mobile/NetworkApp/NetworkApp/tizen_dotnet_project.yaml @@ -0,0 +1,24 @@ +# csproj file path +csproj_file: NetworkApp.csproj + +# Default profile, Tizen API version +profile: tizen +api_version: "9.0" + +# Build type [Debug/ Release/ Test] +build_type: Debug + +# Signing profile to be used for Tizen package signing +# If value is empty: "", active signing profile will be used +# Else If value is ".", default signing profile will be used +signing_profile: . + +# files monitored for dirty/modified status +files: + - NetworkApp.csproj + - NetworkApp.cs + - tizen-manifest.xml + - shared/res/NetworkApp.png + +# project dependencies +deps: [] diff --git a/Mobile/NetworkApp/README.md b/Mobile/NetworkApp/README.md new file mode 100644 index 000000000..84abb562b --- /dev/null +++ b/Mobile/NetworkApp/README.md @@ -0,0 +1,18 @@ +# NetworkApp +The NetworkApp application demonstrates how user can get network information, connect Wi-Fi APs and scan Wi-Fi Direct peer devices. + +![ConnectionPage](./Screenshots/Tizen/NetworkApp-Connection.png) +![WiFiPage](./Screenshots/Tizen/NetworkApp-WiFi.png) +![WiFiDirectPage](./Screenshots/Tizen/NetworkApp-WiFiDirect.png) + + +### Verified Version +* Tizen.NET : 6.0.428 +* Tizen.NET.SDK : 10.0.111 + + +### Supported Profile +* Tizen 10.0 RPI4 + +### Author +* Harish Nanu J diff --git a/Mobile/NetworkApp/Screenshots/Tizen/NetworkApp-Connection.png b/Mobile/NetworkApp/Screenshots/Tizen/NetworkApp-Connection.png new file mode 100755 index 000000000..80810820a Binary files /dev/null and b/Mobile/NetworkApp/Screenshots/Tizen/NetworkApp-Connection.png differ diff --git a/Mobile/NetworkApp/Screenshots/Tizen/NetworkApp-WiFi.png b/Mobile/NetworkApp/Screenshots/Tizen/NetworkApp-WiFi.png new file mode 100755 index 000000000..50bd261d5 Binary files /dev/null and b/Mobile/NetworkApp/Screenshots/Tizen/NetworkApp-WiFi.png differ diff --git a/Mobile/NetworkApp/Screenshots/Tizen/NetworkApp-WiFiDirect.png b/Mobile/NetworkApp/Screenshots/Tizen/NetworkApp-WiFiDirect.png new file mode 100755 index 000000000..0eaf31132 Binary files /dev/null and b/Mobile/NetworkApp/Screenshots/Tizen/NetworkApp-WiFiDirect.png differ