I'm working on PJSIP Android app and facing a problem with call hold. While the caller is call to the receiver when caller is put call on hold, receiver how can identify is remote server call on hold? Which event is occurs in receiver hand?
According to pjsip docs:
virtual void onCallMediaState(OnCallMediaStateParam &prm)
Notify application when media state in the call has changed.
Normal application would need to implement this callback, e.g. to connect the call’s media to sound device.
This method is in Call class in pjsip java:
CallMediaInfo class contains pjsua_call_media_status.
Using pjsua_call_media_status we can know whether current call media status is active or is on call hold.
#Override
public void onCallMediaState(OnCallMediaStateParam prm)
{
CallInfo ci;
try {
ci = getInfo();
} catch (Exception e) {
return;
}
CallMediaInfoVector cmiv = ci.getMedia();
CallMediaInfo callMediaInfo = null;
for (int i = 0; i < cmiv.size(); i++) {
CallMediaInfo cmi = cmiv.get(i);
if (cmi.getType() == pjmedia_type.PJMEDIA_TYPE_AUDIO &&
(cmi.getStatus() ==
pjsua_call_media_status.PJSUA_CALL_MEDIA_REMOTE_HOLD ||
cmi.getStatus() ==
pjsua_call_media_status.PJSUA_CALL_MEDIA_LOCAL_HOLD) )
{
//set ur call status as hold on your TextView
}else if (cmi.getType() == pjmedia_type.PJMEDIA_TYPE_AUDIO &&
(cmi.getStatus() ==
pjsua_call_media_status.PJSUA_CALL_MEDIA_ACTIVE))
{
//set ur call status as connected on your TextView
}
}
}
Related
I have implemented media resumption to show recent tracks after phone restart.
According to dev blog After tapping play button "static media controls will be swapped with the media controls created from your notification" but for me it is not swapped and I have static media control notification and new media notification created by that.
What could be wrong. How the system know what notification should be swapped?
My code:
public BrowserRoot onGetRoot(#NonNull String clientPackageName, int clientUid,
#Nullable Bundle rootHints) {
//ANDROID 11 playback resumption - https://developer.android.com/guide/topics/media/media-controls#java
if (rootHints != null) {
if (rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) {
// Return a tree with a single playable media item for resumption.
Bundle extras = new Bundle();
extras.putBoolean(BrowserRoot.EXTRA_RECENT, true);
KLog.d(clientPackageName + " -> onGetRoot BrowserRoot.EXTRA_RECENT");
return new BrowserRoot(MEDIA_ID_RECENT, extras);
}
}
return new BrowserRoot(MEDIA_ID_ROOT, null);
}
onPlay:
#Override
public void onPlay() {
super.onPlay();
CommonOperations.crashLog("mediaSessionCallback onPlay");
KLog.d("mediaSessionCallback onPlay");
fakeStartForeground();
if (realm == null || realm.isClosed()) {
initRealm();
}
if (playlist != null && currentEpisode != null) {
play();
} else {
List<Episode> unfinished = UserDataManager.getInstance(URLPlayerService.this)
.getUnfinishedEpisodesData();
if (unfinished != null && unfinished.size() > 0) {
EpisodePlaylist list = new EpisodePlaylist(unfinished);
URLPlayerService.startActionSetPlaylist(URLPlayerService.this, list, 0, true);
} else {
KLog.w("stopself");
if (!wasForegroudStart) {
fakeStartForeground();
}
CommonOperations
.crashLog("stopself #" + new Exception().getStackTrace()[0].getLineNumber());
stopSelf();
cancelNotification();
}
}
}
I think I have fixed my issue. There is probably some bud in Android 11 and when I used startForeground with not MediaStyle notification before using MediaStyle notification the problem occurs very often. Even without it I get double notifications from time to time.
I have ended up with using PlayerNotificationManager from ExoPlayer extension.
I am using the https://pub.flutter-io.cn/packages/system_alert_window and it starts a foreground service. This foreground service calls the following callback when a button click event happens in the system alert window.
This is my callback, it is a static method
static Future<void> systemOverlayOnClickListner(String value) async {
if (value == 'button_app_to_foreground') {
await SystemAlertWindow.closeSystemWindow();
await AppAvailability.launchApp('com.company_name.app_name');
}
}
This is the method in the plugin that registers the callback
static Future<bool> registerOnClickListener(
OnClickListener callBackFunction) async {
final callBackDispatcher =
PluginUtilities.getCallbackHandle(callbackDispatcher);
final callBack = PluginUtilities.getCallbackHandle(callBackFunction);
_channel.setMethodCallHandler((MethodCall call) {
print("Got callback");
switch (call.method) {
case "callBack":
dynamic arguments = call.arguments;
if (arguments is List) {
final type = arguments[0];
if (type == "onClick") {
final tag = arguments[1];
callBackFunction(tag);
}
}
}
return null;
});
await _channel.invokeMethod("registerCallBackHandler",
<dynamic>[callBackDispatcher.toRawHandle(), callBack.toRawHandle()]);
return true;
}
And this is the top level method that calls the callback
void callbackDispatcher() {
// 1. Initialize MethodChannel used to communicate with the platform portion of the plugin
const MethodChannel _backgroundChannel =
const MethodChannel(Constants.BACKGROUND_CHANNEL);
// 2. Setup internal state needed for MethodChannels.
WidgetsFlutterBinding.ensureInitialized();
// 3. Listen for background events from the platform portion of the plugin.
_backgroundChannel.setMethodCallHandler((MethodCall call) async {
final args = call.arguments;
// 3.1. Retrieve callback instance for handle.
final Function callback = PluginUtilities.getCallbackFromHandle(
CallbackHandle.fromRawHandle(args[0]));
assert(callback != null);
final type = args[1];
if (type == "onClick") {
final tag = args[2];
// 3.2. Invoke callback.
callback(tag);
}
});
}
But I get the following exception when trying to use a plugin method in the callback
[ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception:
MissingPluginException(No implementation found for method launchApp on
channel com.pichillilorenzo/flutter_appavailability)
From what I can see this is the problem. The registerOnClick listner will invoke a method channel. And this method channel will do the following
case "registerCallBackHandler":
try {
List arguments = (List) call.arguments;
if (arguments != null) {
long callbackHandle = Long.parseLong(String.valueOf(arguments.get(0)));
long onClickHandle = Long.parseLong(String.valueOf(arguments.get(1)));
SharedPreferences preferences = mContext.getSharedPreferences(Constants.SHARED_PREF_SYSTEM_ALERT_WINDOW, 0);
preferences.edit().putLong(Constants.CALLBACK_HANDLE_KEY, callbackHandle)
.putLong(Constants.CODE_CALLBACK_HANDLE_KEY, onClickHandle).apply();
startCallBackHandler(mContext);
result.success(true);
} else {
Log.e(TAG, "Unable to register on click handler. Arguments are null");
result.success(false);
}
}
So startcallback handler is being called
public static void startCallBackHandler(Context context) {
SharedPreferences preferences = context.getSharedPreferences(Constants.SHARED_PREF_SYSTEM_ALERT_WINDOW, 0);
long callBackHandle = preferences.getLong(Constants.CALLBACK_HANDLE_KEY, -1);
Log.d(TAG, "onClickCallBackHandle " + callBackHandle);
if (callBackHandle != -1) {
FlutterMain.ensureInitializationComplete(context, null);
String mAppBundlePath = FlutterMain.findAppBundlePath();
FlutterCallbackInformation flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(callBackHandle);
if (sBackgroundFlutterView == null) {
sBackgroundFlutterView = new FlutterNativeView(context, true);
if(mAppBundlePath != null && !sIsIsolateRunning.get()){
if (sPluginRegistrantCallback == null) {
Log.i(TAG, "Unable to start callBackHandle... as plugin is not registered");
return;
}
Log.i(TAG, "Starting callBackHandle...");
FlutterRunArguments args = new FlutterRunArguments();
args.bundlePath = mAppBundlePath;
args.entrypoint = flutterCallback.callbackName;
args.libraryPath = flutterCallback.callbackLibraryPath;
sBackgroundFlutterView.runFromBundle(args);
sPluginRegistrantCallback.registerWith(sBackgroundFlutterView.getPluginRegistry());
backgroundChannel = new MethodChannel(sBackgroundFlutterView, Constants.BACKGROUND_CHANNEL);
sIsIsolateRunning.set(true);
}
}else {
if(backgroundChannel == null){
backgroundChannel = new MethodChannel(sBackgroundFlutterView, Constants.BACKGROUND_CHANNEL);
}
sIsIsolateRunning.set(true);
}
}
}
This appears to spawn a isolate to run the callback on. So when my callback is triiggered it will be triggered on a seperate isolate.
According to this post
github post
The only way to deal with this is to use the IsolateHandler plugin. But isn't this also a plugin again?
The desired behaviour is that I can call a plugin from the callback.
Note: This happens with any plugin I try to call from the callback
Your error
[ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception:
MissingPluginException(No implementation found for method launchApp on channel
com.pichillilorenzo/flutter_appavailability)
doesn't belong to system alert window plugin.
The reason why you are getting missing plugin exception is that, flutter_appavailability plugin doesn't support background execution.
So for this to work, you have to use isolate communications to bring the callback from system_alert_window to the main thread and then invoke this plugin. Which is mentioned here
An easy way to identify, if a plugin supports background execution is, to see if it is required to register the plugin in application.class or application.kt
I was trying to add sip incoming calls with linphone sdk, The registration is successful and I can make out going calls and the call status is logging as expected, but I am not able to receive incoming calls. I am using intent service to handle connection.
Here is my code:
protected void onHandleIntent(Intent intent) {
String sipAddress = intent.getStringExtra("address");
String password = intent.getStringExtra("password");
final LinphoneCoreFactory lcFactory = LinphoneCoreFactory.instance();
// First instantiate the core Linphone object given only a listener.
// The listener will react to events in Linphone core.
try {
lc = lcFactory.createLinphoneCore(new LinphoneCoreListenerBase() {
#Override
public void callState(LinphoneCore lc, LinphoneCall call, LinphoneCall.State state, String message) {
super.callState(lc, call, state, message);
Log.i(TAG, "callState: ");
}
}, getApplication());
} catch (LinphoneCoreException e) {
e.printStackTrace();
}
lc.setUserAgent("Test app", "1.0");
try {
LinphoneAddress address = lcFactory.createLinphoneAddress(sipAddress);
String username = address.getUserName();
String domain = address.getDomain();
if (password != null) {
lc.addAuthInfo(lcFactory.createAuthInfo(username, password, null, domain));
}
// create proxy config
LinphoneProxyConfig proxyCfg = lc.createProxyConfig(sipAddress, domain, null, true);
proxyCfg.setExpires(2000);
lc.addProxyConfig(proxyCfg); // add it to linphone
lc.setDefaultProxyConfig(proxyCfg);
running = true;
while (running) {
lc.iterate(); // first iterate initiates registration
sleep(20);
}
} catch (LinphoneCoreException e) {
e.printStackTrace();
}
}
What is wrong with my code?
As the IntentService document (https://developer.android.com/reference/android/app/IntentService) stated:
the service is started as needed, handles each Intent in turn using a worker thread, and stops itself when it runs out of work.
I think you should not put the listener in an IntentService. Instead, put it in a long running Service so that the listener can actually keep staying there to receive events.
I'm working in Accessibility Service. i need to get the app name using Accessibility Service. i have studied the documentation of the Accessibility Service in developer's of Android website. but there is no mention about getting the app name using Accessibility.
I also want to extract text from "TextViews" of the other apps(Activities) running in background. How i can do this..
I'm assuming you know how to implement an AccessibilityService.
Retrieving window content:
First register for TYPE_WINDOW_CONTENT_CHANGED events.
#Override
public void onAccessibilityEvent(AccessibilityEvent accessibilityEvent){
int eventType = accessibilityEvent.getEventType();
switch (eventType) {
case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED:
AccessibilityNodeInfo rootNode = getRootInActiveWindow();
ArrayList<AccessibilityNodeInfo> textViewNodes = new ArrayList<AccessibilityNodeInfo>();
findChildViews(rootNode);
for(AccessibilityNodeInfo mNode : textViewNodes){
if(mNode.getText()==null){
return;
}
String tv1Text = mNode.getText().toString();
//do whatever you want with the text content...
}
break;
}
}
Method findChildViews() :
private void findChildViews(AccessibilityNodeInfo parentView) {
if (parentView == null || parentView.getClassName() == null || ) {
return;
}
if (childCount == 0 && (parentView.getClassName().toString().contentEquals("android.widget.TextView"))) {
textViewNodes.add(parentView);
} else {
for (int i = 0; i < childCount; i++) {
findChildViews(parentView.getChild(i));
}
}
}
}
As far as i know, there's no assured way to get the app name, but you can try fetching the text content from the event object you get from TYPE_WINDOW_STATE_CHANGED events.
Try dumping accessibilityEvent.toString() & you'll know what i'm talking about.
Also, accessibilityEvent.getPackageName() is a simple way to get package name of that app; in case you find it useful!
previous answer is missing definition of childCount
int childCount = parentView.getChildCount();
I have been able to successfully cast video to a Chromecast and have the option let the video play when disconnecting and it all works great. However, if I choose to quit the application and let the video continue playing and then try to re-join the currently playing session and try to use the RemoteMediaPlayer to control the video I am getting: "java.lang.IllegalStateException: No current media session".
Just as a background, I am saving the route id and session id on the initial connect into preferences and am able to successfully call "Cast.CastApi.joinApplication" and when in the onResult I am recreating the Media Channel and setting the setMessageReceivedCallbacks like so:
Cast.CastApi.joinApplication(mApiClient,"xxxxxxxx",persistedSessionId).setResultCallback(new ResultCallback<Cast.ApplicationConnectionResult>() {
#Override
public void onResult(Cast.ApplicationConnectionResult applicationConnectionResult) {
Status status = applicationConnectionResult.getStatus();
if (status.isSuccess()) {
mRemoteMediaPlayer = new RemoteMediaPlayer();
mRemoteMediaPlayer.setOnStatusUpdatedListener(
new RemoteMediaPlayer.OnStatusUpdatedListener() {
#Override
public void onStatusUpdated() {
Log.d("----Chromecast----", "in onStatusUpdated");
}
});
mRemoteMediaPlayer.setOnMetadataUpdatedListener(
new RemoteMediaPlayer.OnMetadataUpdatedListener() {
#Override
public void onMetadataUpdated() {
Log.d("----Chromecast----", "in onMetadataUpdated");
}
});
try {
Cast.CastApi.setMessageReceivedCallbacks(mApiClient,mRemoteMediaPlayer.getNamespace(), mRemoteMediaPlayer);
} catch (IOException e) {
Log.e("----Chromecast----", "Exception while creating media channel", e);
}
//-----------RESOLUTION START EDIT------------------
mRemoteMediaPlayer.requestStatus(mApiClient).setResultCallback(new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
#Override
public void onResult(RemoteMediaPlayer.MediaChannelResult mediaChannelResult) {
Status stat = mediaChannelResult.getStatus();
if(stat.isSuccess()){
Log.d("----Chromecast----", "mMediaPlayer getMediaStatus success");
// Enable controls
}else{
Log.d("----Chromecast----", "mMediaPlayer getMediaStatus failure");
// Disable controls and handle failure
}
}
});
//-----------RESOLUTION END EDIT------------------
}else{
Log.d("----Chromecast----", "in status failed");
}
}
}
If I declare the RemoteMediaPlayer as static:
private static RemoteMediaPlayer mRemoteMediaPlayer;
I can join the existing session as well as control the media using commands like:
mRemoteMediaPlayer.play(mApiClient);
or
mRemoteMediaPlayer.pause(mApiClient);
But once I quit the application obviously the static object is destroyed and the app produces the aforementioned "No current media session" exception. I am definitely missing something because after I join the session and register the callback perhaps I need to start the session just like it was creating when I initially loaded the media using mRemoteMediaPlayer.load(.
Can someone please help as this is very frustrating?
The media session ID is part of the internal state of the RemoteMediaPlayer object. Whenever the receiver state changes, it sends updated state information to the sender, which then causes the internal state of the RemoteMediaPlayer object to get updated.
If you disconnect from the application, then this state inside the RemoteMediaPlayer will be cleared.
When you re-establish the connection to the (still running) receiver application, you need to call RemoteMediaPlayer.requestStatus() and wait for the OnStatusUpdatedListener.onStatusUpdated() callback. This will fetch the current media status (including the current session ID) from the receiver and update the internal state of the RemoteMediaPlayer object accordingly. Once this is done, if RemoteMediaPlayer.getMediaStatus() returns non-null, then it means that there is an active media session that you can control.
As user3408864 pointed out, requestStatus() after rejoining the session works. Here is how i managed to solve it in my case and it should work in yours.
if(MAIN_ACTIVITY.isConnected()){
if(MAIN_ACTIVITY.mRemoteMediaPlayer == null){
MAIN_ACTIVITY.setRemoteMediaPlayer();
}
MAIN_ACTIVITY.mRemoteMediaPlayer.requestStatus(MAIN_ACTIVITY.mApiClient).setResultCallback( new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
#Override
public void onResult(RemoteMediaPlayer.MediaChannelResult mediaChannelResult) {
if(playToggle ==0){
try {
MAIN_ACTIVITY.mRemoteMediaPlayer.pause(MAIN_ACTIVITY.mApiClient);
playToggle =1;
} catch (IOException e) {
e.printStackTrace();
}
}else{
try {
MAIN_ACTIVITY.mRemoteMediaPlayer.play(MAIN_ACTIVITY.mApiClient);
playToggle =0;
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
Ignore, MAIN_ACTIVITY, it is just a static reference to my activity since i run this piece of code from a Service. Also, setRemoteMediaPlayer() is a method where i create a new RemoteMediaPlayer() and attach the corresponding Listeners.
Hopefully this helps. Also, sorry if any mistake, it is my first post to StackOverFlow.