Xamarin.Forms ViewRenderer audio doesnt stop on homepage/backbutton - android

I am new so excuse me if I dont ask a question right or post enough information.
I am new to creating mobile applications and i am using Xamarin.Forms to create a custom view. with this view I am using an Android ViewRenderer to play audio/video with built in android MediaPlayer/VideoView.
pretty much the exact same thing as the android renderer posted and accepted as the answer for Renderer I copied and is working
My issue is when the video starts and you click the homepage/back button the audio continues playing for a few seconds and then stops. I want to audio to stop immediately.
Methods I have tried:
In my ViewRenderer I have attempted to override SurfaceDestroyed to call player.stop(). This has not worked, no errors or anything just audio continues like this code doesnt exist. Audio stops after about 3-5 seconds.
In the ViewRenderer I have attempted to use the Control.SystemUiVisibilityChange event to call player.stop(). No errors or anything. Audio continues for 3-5 seconds.
I am unable to pass the player instance to the main activity onPause() method as I am to new to Xamarin.Forms and android ViewRenderers to understand how to. Possibly calling this player.stop() on the onPause() method will work but I cant find how to do this. Can anyone assist? I have searched many forums for weeks and have finally given up to post a question.

For back button, you simply need to override OnBackButtonPressed of your current Xamarin.Forms' page:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
protected override bool OnBackButtonPressed()
{
//stop the videoview
videoview.Stop();
return base.OnBackButtonPressed();
}
...
}
For home button, I referred to this thread and made a Xamarin version of HomeWatcher out of Jack's answer:
public interface IOnHomePressedListener
{
void OnHomePressed();
void OnHomeLongPressed();
}
public class HomeWatcher
{
static readonly String TAG = "hg";
private Context mContext;
private IntentFilter mFilter;
private IOnHomePressedListener mListener;
private InnerRecevier mRecevier;
public HomeWatcher(Context context)
{
mContext = context;
mFilter = new IntentFilter(Intent.ActionCloseSystemDialogs);
}
public void SetOnHomePressedListener(IOnHomePressedListener listener)
{
mListener = listener;
mRecevier = new InnerRecevier(mListener);
}
public void StartWatch()
{
if (mRecevier != null)
{
mContext.RegisterReceiver(mRecevier, mFilter);
}
}
public void StopWatch()
{
if (mRecevier != null)
{
mContext.UnregisterReceiver(mRecevier);
}
}
private class InnerRecevier : BroadcastReceiver
{
readonly String SYSTEM_DIALOG_REASON_KEY = "reason";
readonly String SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS = "globalactions";
readonly String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
readonly String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
IOnHomePressedListener _listener;
public InnerRecevier(IOnHomePressedListener listener)
{
_listener = listener;
}
public override void OnReceive(Context context, Intent intent)
{
String action = intent.Action;
if (action.Equals(Intent.ActionCloseSystemDialogs))
{
String reason = intent.GetStringExtra(SYSTEM_DIALOG_REASON_KEY);
if (reason != null)
{
//Log.e(TAG, "action:" + action + ",reason:" + reason);
if (_listener != null)
{
if (reason.Equals(SYSTEM_DIALOG_REASON_HOME_KEY))
{
_listener.OnHomePressed();
}
else if (reason.Equals(SYSTEM_DIALOG_REASON_RECENT_APPS))
{
_listener.OnHomeLongPressed();
}
}
}
}
}
}
}
And use it in the VideoViewRenderer ( StartWatch() when video start play, StopWatch() when the videoview is cleaned):
public class VideoViewRenderer : ViewRenderer<VideoView, Android.Widget.VideoView>, ISurfaceHolderCallback,IOnHomePressedListener
{
...
private MediaPlayer _player;
private HomeWatcher _homeWatcher;
public VideoViewRenderer(Context context) : base(context)
{
_context = context;
_homeWatcher = new HomeWatcher(context);
_homeWatcher.SetOnHomePressedListener(this);
}
protected override void OnElementChanged(ElementChangedEventArgs<CustomVideoViewDemo.VideoView> e)
{
base.OnElementChanged(e);
e.NewElement.CleanAction = new Action(() =>
{
#region Clean video player action (player no more used)
if (_player == null)
return;
//stop watch home button
_homeWatcher.StopWatch();
_player.Release();
#endregion
});
e.NewElement.PlayAction = new Action(() =>
{
#region Play video if it was stopped
if (_player == null)
return;
//start watch home button
_homeWatcher.StartWatch();
if (!_player.IsPlaying)
{
_player.Start();
}
#endregion
});
...
}
}

Related

Porting View Renderer in .net

I am currently migrating my Xamarin.Forms app to .NET MAUI, and having a difficulty in migrating view renderer. In .NET MAUI I am using camera2 in my app, and using the renderer for same.
My Xamarin forms code is
public class CameraRecordV3 : View
{
public static readonly BindableProperty StartProperty = BindableProperty.Create(
"Start", typeof(int), typeof(int), 6000);
public int Start
{
set { SetValue(StartProperty, value); }
get { return (int)GetValue(StartProperty); }
}
}
using iVue.Views;
using System.ComponentModel;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Controls.Handlers.Compatibility;
namespace iVue.Platforms.Android.Renderers;
public class CameraRecordRenderer_V3 : ViewRenderer<CameraRecordV3, CameraRecordControl_V3>
{
private CameraRecordControl_V3 _cameraControl;
private DisplayTimeHelper _displayTimeHelper = new DisplayTimeHelper();
public CameraRecordRenderer_V3(Context context)
: base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<CameraRecordV3> e)
{
base.OnElementChanged(e);
if (Control == null)
{
_cameraControl = new CameraRecordControl_V3(Context, e.NewElement);
SetNativeControl(_cameraControl);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var model = (CameraRecordV3)sender;
base.OnElementPropertyChanged(sender, e);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_cameraControl.Dispose();
if(Control != null)
Control.Dispose();
}
}
}
CameraRecordControl_V3 is a viewgroup which contains a native view for android, which contains buttons and camera
public class CameraRecordControl_V3 : ViewGroup
{
public CameraRecordControl_V3(Context context, CameraRecordV3 vm) : base(context)
{
_activity = this.Context as Activity;
_view = _activity.LayoutInflater.Inflate(Resource.Layout.CameraRecordLayoutV2, this, false);
AddView(_view);
_toolbar = (Toolbar)_view.FindViewById(Resource.Id.toolbar);
textureView = (AutoFitTextureView)_view.FindViewById(Resource.Id.textureview)
_questionTitleView = (Button)_view.FindViewById(Resource.Id.Start);
}
}
I tried using handler in .net maui but no luck with it.
My Maui Code is as follows
public interface ICameraRecordV3 : IView
{
public int StartTime { get; }
}
public partial class CameraRecordV3Handler
{
public static PropertyMapper<ICameraRecordV3, CameraRecordV3Handler> CustomMapper
= new PropertyMapper<ICameraRecordV3, CameraRecordV3Handler>(ViewHandler.ViewMapper)
{
[nameof(ICameraRecordV3.StartTime)] = MapStartTime,
};
public CameraRecordV3Handler() : base(CustomMapper)
{
}
public CameraRecordV3Handler(PropertyMapper mapper = null) : base(mapper ?? CustomMapper)
{
}
}
public class CameraRecordV3 : View, ICameraRecordV3
{
public static readonly BindableProperty StartProperty = BindableProperty.Create(
"StartTime", typeof(int), typeof(int), 6000);
public int Start
{
set { SetValue(StartTimeProperty, value); }
get { return (int)GetValue(StartTimeProperty); }
}
}
//Platform Specific code
public partial class CameraRecordV3Handler : ViewHandler<ICameraRecordV3, CameraRecordControl_V3>
{
private CameraRecordControl_V3 _cameraControl;
protected override CameraRecordControl_V3 CreatePlatformView()
{
_cameraControl = new CameraRecordControl_V3(Context, null);
return _cameraControl;
}
protected override void ConnectHandler(CameraRecordControl_V3 platformView)
{
base.ConnectHandler(platformView);
}
private static void MapStartTime(CameraRecordV3Handler handler, ICameraRecordV3 arg2)
{
handler.PlatformView?.UpdateStartTime(arg2.StartTime);
}
}
//MauiProgram
builder.ConfigureMauiHandlers(handlers =>
{
#if __ANDROID__
handlers.AddHandler(typeof(CameraRecordV3), typeof(iVue.Handlers.CameraRecordV3Handler));
#endif
});
You can continue to use CustomRenderer in MAUI, you just need to Remove any ExportRenderer directives as they won't be needed in .NET MAUI. And then configure each renderer using conditional compilation for each platform. You can replace handlers.AddCompatibilityRenderer with handlers.AddHandler in the documentation. Using handlers.AddCompatibilityRenderer will cause a crash.

Android MediaPlayer dispose of video correctly

I am trying to create a Xamarin Forms application that displays a full screen video for the launch page. Once loading is complete the launch page takes you to the home screen. The home screen displays a series of videos in a carousel.
On Android the carousel videos work fine by themselves but when I add in the launch screen video it displays over the carousel ones. Currently I have a View called VideoView in my App that has a BindableProperty for the video source.
The rendering is done platform specifically in a custom renderer that is below. It uses a native VideoView and MediaPlayer control for Android. I have tried the various dispose and and release functions in the OnDisappearing function in the launch screen, but none of them seem to release the video.
[assembly: ExportRenderer(typeof(App.Renderers.VideoView),
typeof(VideoViewRenderer))]
namespace App.Droid.Renderers
{
public class VideoViewRenderer :
ViewRenderer<App.Renderers.VideoView, VideoView>, ISurfaceHolderCallback
{
private VideoView videoview;
private MediaPlayer player;
public void SurfaceChanged(ISurfaceHolder holder, global::Android.Graphics.Format format, int width, int height)
{
}
public void SurfaceDestroyed(ISurfaceHolder holder)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<App.Renderers.VideoView> e)
{
base.OnElementChanged(e);
e.NewElement.StopAction = () =>
{
player.Pause();
};
e.NewElement.StartAction = () =>
{
player.Start();
};
e.NewElement.DisposeAction = () =>
{
Control.StopPlayback();
Control.Dispose();
player.Stop();
player.Reset();
player.Release();
};
if (Control == null)
{
videoview = new VideoView(Context);
base.SetNativeControl(videoview);
Control.Holder.AddCallback(this);
player = new MediaPlayer();
play(e.NewElement.FileSource);
}
}
void play(string fullPath)
{
AssetFileDescriptor afd = Forms.Context.Assets.OpenFd(fullPath);
if (afd != null)
{
try
{
player.SetDataSource(afd.FileDescriptor, afd.StartOffset, afd.Length);
player.Prepare();
player.Looping = true;
Control.Layout(0, 200, player.VideoHeight, player.VideoWidth);
player.SetVideoScalingMode(VideoScalingMode.ScaleToFit);
}
catch
{
}
}
}
public void SurfaceCreated(ISurfaceHolder holder)
{
player.SetDisplay(holder);
}
}
}
The VideoView class extends View and is as follows:
public class VideoView : View
{
public Action StopAction;
public Action StartAction;
public Action DisposeAction;
public VideoView()
{
FileSource = string.Empty;
}
public static readonly BindableProperty FileSourceProperty =
BindableProperty.Create<VideoView, string>(
p => p.FileSource, string.Empty);
public string FileSource
{
get { return (string)GetValue(FileSourceProperty); }
set { SetValue(FileSourceProperty, value); }
}
public void Stop()
{
if (StopAction != null)
StopAction();
}
public void Dispose()
{
if (DisposeAction != null)
DisposeAction();
}
public void Start()
{
if (StartAction != null)
StartAction();
}
}
And finally usage of the view is as follows:
<controls:VideoView Grid.Row="0" Grid.Column="0" x:Name="Video1" FileSource="big_buck_bunny.mp4" Margin="0,10,0,40" />

Getting results of service in MainActivity

How to continuously get the results from a service to UI or MainActivity.?
I am copying some large file using a service,and i want to update the progress bar in Foreground.
Simplest way to do this is to use Singleton object with listener.
Subscribe for update in activity and just set values in Service.
public class ProgressSingleton {
private static ProgressSingleton instance;
private ProgressListener progressListener = null;
public static void initInstance() {
if (instance == null) {
instance = new ProgressSingleton();
}
}
public static ProgressSingleton getInstance() {
return instance;
}
private ProgressSingleton() {
}
public void setProgress(int progress) {
if (progressListener != null) {
progressListener.onProgressUpdated(progress);
}
}
public interface ProgressListener {
void onProgressUpdated(int progress);
}
}
Make shure that you handle updates from listener in UI thread.
Also you can start your service in foreground, best option for big files.

Configuring RxJava to Send Data to activity from GCMListenerService

I am trying to send an update to my Activity from my GCMServiceListener so, I am using RxJava/RxAndroid And created a BusClass for handling sending and Observers
public class ClientBus {
//private final PublishSubject<Object> _bus = PublishSubject.create();
// If multiple threads are going to emit events to this
// then it must be made thread-safe like this instead
private final Subject<Object, Object> _bus = new SerializedSubject<>(PublishSubject.create());
public void send(Object o) {
_bus.onNext(o);
}
public Observable<Object> toObserverable() {
return _bus;
}
public boolean hasObservers() {
return _bus.hasObservers();
}
}
And in my Application Class I did this to initialize the BusClass
private ClientBus clientBus;
public ClientBus getRxBusSingleton() {
if (clientBus == null) {
clientBus = new ClientBus();
}
return clientBus;
}
In the activity I want to receive the message, I registered a CompositeSubscription and get a reference to my ClientBus class from the Application Class
clientBus = ((MyApplication) getApplicationContext()).getRxBusSingleton();
#Override
protected void onStart() {
super.onStart();
initSubscriptions();
}
#Override
protected void onStop() {
super.onStop();
_subscriptions.unsubscribe();
}
void initSubscriptions() {
_subscriptions = new CompositeSubscription();
_subscriptions.add(clientBus.toObserverable().subscribe(new Action1<Object>() {
#Override
public void call(Object event) {
Log.e("New Event", "Event Received");
if (event instanceof MyGcmListenerService.Message) {
String msg = ((MyGcmListenerService.Message) event).getMessage();
if (msg.equals("Update Available")) {
scheduleArrayList = getSchedules();
scheduleAdapter = new ScheduleAdapter(getApplicationContext(), scheduleArrayList, ScheduledUberActivity.this);
scheduledList.setAdapter(scheduleAdapter);
scheduleAdapter.notifyDataSetChanged();
} else if (msg.equals("Refresh")) {
fetchTrips();
}
}
}
}));
}
And from the MyGcmListenerService class I did this when I get a new notification
private void sendRefreshNotif() {
if (clientBus.hasObservers()) {<--It enters the if cause the Log prints. But, the activity doesn't get the message
Log.e("Obervers", "Observers aren't null");
clientBus.send(new Message("Refresh"));
}
}
What I don't understand is why isn't it working here? I use it to interact between activities and fragments. I closed my application to check if the notification comes in, It'll enter this block if (clientBus.hasObservers()) { but it didn't and starting the app and testing the Observer, it notices there's an active Observer. Any help? Thanks.
It seems like you used different instances of the ClientBus class in CompositeSubscription and MyApplication.
Try to make a singleton from ClientBus class, it works fine for me.
public class ClientBus {
public ClientBus(SingletonAccessor accessor) {}
private static ClientBus instance;
private static class SingletonAccessor{}
public static ClientBus getInstance() {
if (instance == null) instance = new ClientBus(new SingletonAccessor());
return instance;
}
private final Subject<Object, Object> mBus = new SerializedSubject<>(PublishSubject.create());
public void send(Object o) {
mBus.onNext(o);
}
public Observable<Object> toObserverable() {
return mBus;
}
public boolean hasObservers() {
return mBus.hasObservers();
}
}

Using Espresso idling resource with multiple activities

I have a firstActivity that launches the secondActivity, where in the secondActivity I have a loading Dialog (not AsyncTask), and I need to make Espresso wait until the dialog disappears before it continues with the test.
Where do I have to implement the IdlingResource? How can I make it wait for the dismissDialog() function?
Here is what I've tried to do:
class DocumentLoadingIdlingResource implements IdlingResource {
private ResourceCallback callback;
#Override
public String getName() {
return "Documnet loading idling resource";
}
#Override
public boolean isIdleNow() {
Activity activity;
try {
activity = getCurrentActivity();
} catch (Throwable e) {
return false;
}
if(activity.getClass().getName().equals(EditorActivity.class.getName())
&& activity.loadingDialogShowing() == false) {
return false;
}
return true;
}
#Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
this.callback = callback;
}
}
Activity getCurrentActivity() throws Throwable {
getInstrumentation().waitForIdleSync();
final Activity[] activity = new Activity[1];
runTestOnUiThread(new Runnable() {
#Override
public void run() {
java.util.Collection<Activity> activites = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
activity[0] = com.google.common.collect.Iterables.getOnlyElement(activites);
}});
return activity[0];
}
This class is implemented in the test class.
There are a few problems here:
Your isIdleNow() calls getCurrentActivity() which calls waitForIdleSync() and runTestOnUiThread(). isIdleNow Javadoc says: "Espresso will always call this method from the main thread, therefore it should be non-blocking and return immediately." So this won't work as is, but you could call getActivitiesInStage directly from isIdleNow.
Your other issue is that you store the reference to ResourceCallback but never invoke onTransitionToIdle, also you should allow for the possibility of more than one ResourceCallback being registered and call onTransitionToIdle on all of the callbacks.
You can do the following:
Copy/Paste IdlingResource into your app as com.mycompany.IdlingResource.
Then have your Activity implement that interface and make sure to call onTransitionToIdle when the dialog goes away and make sure isIdleNow returns false iff the dialog is showing.
In your test code, write a "IdlingResourceAdapter" that wraps com.mycompany.IdlingResource and turns it into an Espresso IdlingResource and register that with Espresso.
This will be simpler once this issue is implemented: https://code.google.com/p/android-test-kit/issues/detail?id=71
I stumbled upon this question in my search for a similar answer. Using concepts from Stefano Dacchille's article on IdlingResources, I built the following idling resource that waits for a specific Activity to be active before firing. In my case, I know the dialog is showing when a fragment with a specific tag exists. This isn't the same as the OP's test, but the concepts should translate well.
public class BusyWhenFragmentExistsInActivityIdlingResource implements IdlingResource {
private FragmentActivity activity = null;
private final String fragmentTag;
private ResourceCallback resourceCallback;
private boolean wasIdleLastTime = true; // Start off as idle
private final String name;
// Need this strong reference because ActivityLifecycleMonitorRegistry won't hold one
private final ActivityLifecycleCallback activityLifecycleCallback;
public BusyWhenFragmentExistsInActivityIdlingResource(
final Class<? extends FragmentActivity> clazz,
final String fragmentTag
){
name = BusyWhenFragmentExistsInActivityIdlingResource.class.getSimpleName()+" "+clazz.getSimpleName();
this.fragmentTag = fragmentTag;
activityLifecycleCallback = new ActivityLifecycleCallback() {
#Override
public void onActivityLifecycleChanged(Activity activity, Stage stage) {
if (!FragmentActivity.class.isAssignableFrom(activity.getClass())) {
return;
}
FragmentActivity fragmentActivity = (FragmentActivity) activity;
if (!clazz.isAssignableFrom(fragmentActivity.getClass())) {
return;
}
switch (stage){
case RESUMED:
BusyWhenFragmentExistsInActivityIdlingResource.this.activity = fragmentActivity;
break;
case STOPPED:
BusyWhenFragmentExistsInActivityIdlingResource.this.activity = null;
break;
}
}
};
ActivityLifecycleMonitorRegistry.getInstance()
.addLifecycleCallback(activityLifecycleCallback);
}
#Override
public String getName() {
return name;
}
#Override
public boolean isIdleNow() {
if (activity==null) {
return wasIdleLastTime = true;
}
boolean isIdleThisTime = activity
.getSupportFragmentManager()
.findFragmentByTag(fragmentTag)==null;
if (!wasIdleLastTime && isIdleThisTime && resourceCallback!=null){
resourceCallback.onTransitionToIdle();
}
return wasIdleLastTime = isIdleThisTime;
}
#Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
}
To use it, add something similar to this to your test:
#Before
public void setUp() throws Exception {
registerIdlingResources(new BusyWhenFragmentExistsInActivityIdlingResource(
SomeOtherActivity.class,
BaseActivity.LOADING_DIALOG
));
}

Categories

Resources