video: add basic support
* implement device enumeration DRing API
* implement frame capture DRing API
* add basic video UI
Tuleap: #293
Change-Id: Ia9a364da70f76f1bb9d26b422facefaa9c25ce76
diff --git a/ring-android/app/src/main/AndroidManifest.xml b/ring-android/app/src/main/AndroidManifest.xml
index 90255e9..5dadb98 100644
--- a/ring-android/app/src/main/AndroidManifest.xml
+++ b/ring-android/app/src/main/AndroidManifest.xml
@@ -52,6 +52,8 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-feature
android:name="android.hardware.wifi"
@@ -173,8 +175,9 @@
<activity
android:name=".client.CallActivity"
android:label="@string/app_name"
- android:screenOrientation="portrait"
- android:theme="@style/AppThemeWithoutOverlayCompat"
+ android:screenOrientation="fullSensor"
+ android:configChanges="orientation|keyboardHidden|screenSize"
+ android:theme="@style/AppTheme.ActionBar.Transparent"
android:windowSoftInputMode="adjustPan">
<intent-filter>
diff --git a/ring-android/app/src/main/java/cx/ring/client/CallActivity.java b/ring-android/app/src/main/java/cx/ring/client/CallActivity.java
index 55d5d31..69f9c2a 100644
--- a/ring-android/app/src/main/java/cx/ring/client/CallActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/client/CallActivity.java
@@ -22,6 +22,10 @@
package cx.ring.client;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.res.Configuration;
+import android.os.Build;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
@@ -49,6 +53,7 @@
import android.os.SystemClock;
import android.util.Pair;
import android.view.KeyEvent;
+import android.view.View;
import android.view.Window;
import android.view.WindowManager;
@@ -60,22 +65,49 @@
public static final String ACTION_CALL = BuildConfig.APPLICATION_ID + ".action.call";
private boolean init = false;
+ private View mainView;
+
private LocalService service;
private CallFragment mCurrentCallFragment;
private Conference mDisplayedConference;
+ private String savedConferenceId = null;
/* result code sent in case of call failure */
public static int RESULT_FAILURE = -10;
private CallProximityManager mProximityManager = null;
+ private int currentOrientation = Configuration.ORIENTATION_PORTRAIT;
+ private boolean dimmed = false;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
- Log.i(TAG, "CallActivity onCreate");
super.onCreate(savedInstanceState);
+ if (savedInstanceState != null)
+ savedConferenceId = savedInstanceState.getString("conference", null);
+ Log.i(TAG, "CallActivity onCreate " + savedConferenceId);
+ int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
Window w = getWindow();
- w.setFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED, WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ w.setFlags(flags, flags);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ w.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ w.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
+ }
+
+ setContentView(R.layout.activity_call_layout);
+ mainView = findViewById(R.id.maincalllayout);
+ mainView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dimmed = !dimmed;
+ if (dimmed) {
+ hideSystemUI();
+ } else {
+ showSystemUI();
+ }
+ }
+ });
Intent intent = new Intent(this, LocalService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
@@ -88,6 +120,68 @@
window.setFormat(PixelFormat.RGBA_8888);
}
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ Log.w(TAG, "onConfigurationChanged " + newConfig.screenWidthDp);
+
+ currentOrientation = newConfig.orientation;
+
+ // Checks the orientation of the screen
+ if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+ dimmed = true;
+ hideSystemUI();
+ } else if (currentOrientation == Configuration.ORIENTATION_PORTRAIT){
+ dimmed = false;
+ showSystemUI();
+ }
+
+ //hideSystemUI();
+ super.onConfigurationChanged(newConfig);
+ }
+
+ // This snippet hides the system bars.
+ private void hideSystemUI() {
+ // Set the IMMERSIVE flag.
+ // Set the content to appear under the system bars so that the content
+ // doesn't resize when the system bars hide and show.
+ if (mainView != null) {
+ mainView.setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LOW_PROFILE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
+ | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
+ | View.SYSTEM_UI_FLAG_IMMERSIVE);
+ }
+ }
+
+ // This snippet shows the system bars. It does this by removing all the flags
+// except for the ones that make the content appear under the system bars.
+ private void showSystemUI() {
+ if (mainView != null) {
+ mainView.setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LOW_PROFILE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_IMMERSIVE);
+ }
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (hasFocus && currentOrientation == Configuration.ORIENTATION_LANDSCAPE)
+ hideSystemUI();
+ else
+ showSystemUI();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString("conference", mDisplayedConference.getId());
+ }
+
private Handler mHandler = new Handler();
private Runnable mUpdateTimeTask = new Runnable() {
@Override
@@ -140,7 +234,11 @@
mProximityManager = new CallProximityManager(CallActivity.this, CallActivity.this);
mProximityManager.startTracking();
- checkExternalCall();
+ if (savedConferenceId != null) {
+ mDisplayedConference = service.getConference(savedConferenceId);
+ } else {
+ checkExternalCall();
+ }
if (mDisplayedConference == null || mDisplayedConference.getParticipants().isEmpty()) {
CallActivity.this.finish();
@@ -151,12 +249,16 @@
init = true;
}
- setContentView(R.layout.activity_call_layout);
- mCurrentCallFragment = (CallFragment) getFragmentManager().findFragmentById(R.id.ongoingcall_pane);
+ FragmentManager fragmentManager = getFragmentManager();
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+ mCurrentCallFragment = new CallFragment();
+ fragmentTransaction.add(R.id.maincalllayout, mCurrentCallFragment).commit();
+ hideSystemUI();
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
+
}
};
@@ -228,9 +330,7 @@
@Override
public void updateDisplayedConference(Conference c) {
- if(mDisplayedConference.equals(c)){
- mDisplayedConference = c;
- }
+ mDisplayedConference = c;
}
@Override
diff --git a/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java b/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java
index adcd84a..d9f715e 100644
--- a/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/client/ConversationActivity.java
@@ -108,6 +108,7 @@
Conversation conv = s.getConversation(conv_id);
if (conv == null) {
long contact_id = CallContact.contactIdFromId(conv_id);
+ Log.w(TAG, "no conversation found, contact_id " + contact_id);
CallContact contact = null;
if (contact_id >= 0)
contact = s.findContactById(contact_id);
@@ -119,13 +120,17 @@
contact = CallContact.buildUnknown(conv_uri);
} else {
contact = s.findContactByNumber(conv_uri);
- if (contact == null)
+ if (contact == null) {
contact = CallContact.buildUnknown(conv_uri);
- number = contact.getPhones().get(0).getNumber();
+ number = contact.getPhones().get(0).getNumber();
+ } else {
+ number = conv_uri;
+ }
}
}
conv = s.startConversation(contact);
}
+ Log.w(TAG, "returning " + conv.getContact().getDisplayName() + " " + number);
return new Pair<>(conv, number);
}
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/AccountCreationFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/AccountCreationFragment.java
index 691b2de..d17a98e 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/AccountCreationFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/AccountCreationFragment.java
@@ -196,7 +196,7 @@
HashMap<String, String> accountDetails = (HashMap<String, String>) mCallbacks.getRemoteService().getAccountTemplate(mAccountType);
accountDetails.put(AccountDetailBasic.CONFIG_ACCOUNT_TYPE, mAccountType);
- accountDetails.put(AccountDetailBasic.CONFIG_VIDEO_ENABLED, "false");
+ accountDetails.put(AccountDetailBasic.CONFIG_VIDEO_ENABLED, "true");
if (mAccountType.equals("RING")) {
accountDetails.put(AccountDetailBasic.CONFIG_ACCOUNT_ALIAS, "Ring");
accountDetails.put(AccountDetailBasic.CONFIG_ACCOUNT_HOSTNAME, "bootstrap.ring.cx");
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/AudioManagementFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/AudioManagementFragment.java
index 2c8d31e..854ee6a 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/AudioManagementFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/AudioManagementFragment.java
@@ -408,7 +408,7 @@
public void setDataset(ArrayList<Codec> codecs) {
items = new ArrayList<>(codecs.size());
for (Codec c : codecs)
- if (c.getType() == Codec.Type.AUDIO)
+ //if (c.getType() == Codec.Type.AUDIO)
items.add(c);
}
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java
index 1f0dbb5..5102b3c 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/CallFragment.java
@@ -22,17 +22,22 @@
package cx.ring.fragments;
import android.app.Activity;
+import android.app.Fragment;
+import android.content.BroadcastReceiver;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
import android.media.AudioManager;
import android.net.Uri;
import android.support.v4.app.NotificationManagerCompat;
import android.content.Context;
import android.content.Intent;
-import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.RemoteException;
import android.support.v7.app.ActionBar;
+import android.util.DisplayMetrics;
import android.util.Log;
import android.view.*;
import android.view.View.OnClickListener;
@@ -43,15 +48,20 @@
import cx.ring.client.HomeActivity;
import cx.ring.interfaces.CallInterface;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
import java.util.Locale;
import cx.ring.model.CallContact;
import cx.ring.model.Conference;
import cx.ring.model.SecureSipCall;
import cx.ring.model.SipCall;
+import cx.ring.service.CallManagerCallBack;
+import cx.ring.service.DRingService;
+import cx.ring.service.IDRingService;
import cx.ring.service.LocalService;
-public class CallFragment extends CallableWrapperFragment implements CallInterface {
+public class CallFragment extends Fragment implements CallInterface {
static private final String TAG = CallFragment.class.getSimpleName();
@@ -59,6 +69,7 @@
// Screen wake lock for incoming call
private WakeLock mScreenWakeLock;
+ private View contactBubbleLayout;
private ImageView contactBubbleView;
private TextView contactBubbleTxt;
private TextView contactBubbleNumTxt;
@@ -68,6 +79,8 @@
private View securityIndicator;
private MenuItem speakerPhoneBtn = null;
private MenuItem addContactBtn = null;
+ private SurfaceView video = null;
+ private SurfaceView videoPreview = null;
ViewSwitcher mSecuritySwitch;
private TextView mCallStatusTxt;
@@ -76,7 +89,49 @@
private AudioManager audioManager;
- TransferDFragment editName;
+ private boolean haveVideo = false;
+ private int videoWidth = -1, videoHeight = -1;
+ private int previewWidth = -1, previewHeight = -1;
+ private boolean lastVideoSource = true;
+
+ private Conference mCachedConference = null;
+ private ViewGroup rootView = null;
+ private boolean ongoingCall = false;
+
+ @Override
+ public void onAttach(Activity activity) {
+ Log.i(TAG, "onAttach");
+ super.onAttach(activity);
+
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(CallManagerCallBack.RECORD_STATE_CHANGED);
+ intentFilter.addAction(CallManagerCallBack.ZRTP_OFF);
+ intentFilter.addAction(CallManagerCallBack.ZRTP_ON);
+ intentFilter.addAction(CallManagerCallBack.DISPLAY_SAS);
+ intentFilter.addAction(CallManagerCallBack.ZRTP_NEGOTIATION_FAILED);
+ intentFilter.addAction(CallManagerCallBack.ZRTP_NOT_SUPPORTED);
+ intentFilter.addAction(CallManagerCallBack.RTCP_REPORT_RECEIVED);
+
+ intentFilter.addAction(DRingService.VIDEO_EVENT);
+
+ intentFilter.addAction(LocalService.ACTION_CONF_UPDATE);
+
+ getActivity().registerReceiver(mReceiver, intentFilter);
+ }
+
+ @Override
+ public void onDetach() {
+ Log.i(TAG, "onDetach");
+ getActivity().unregisterReceiver(mReceiver);
+ mCallbacks = sDummyCallbacks;
+ super.onDetach();
+ }
@Override
public void onCreate(Bundle savedBundle) {
@@ -87,7 +142,7 @@
setHasOptionsMenu(true);
PowerManager powerManager = (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
- mScreenWakeLock = powerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE,
+ mScreenWakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE,
"cx.ring.onIncomingCall");
mScreenWakeLock.setReferenceCounted(false);
@@ -95,6 +150,17 @@
if (mScreenWakeLock != null && !mScreenWakeLock.isHeld()) {
mScreenWakeLock.acquire();
}
+
+ setRetainInstance(true);
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.i(TAG, "onDestroy");
+ if (mScreenWakeLock != null && mScreenWakeLock.isHeld()) {
+ mScreenWakeLock.release();
+ }
+ super.onDestroy();
}
/**
@@ -127,20 +193,71 @@
}
private static final Callbacks sDummyCallbacks = new DummyCallbacks();
- @Override
- public void onAttach(Activity activity) {
- Log.i(TAG, "onAttach");
- super.onAttach(activity);
+ public class CallReceiver extends BroadcastReceiver {
+ private final String TAG = CallReceiver.class.getSimpleName();
- if (!(activity instanceof Callbacks)) {
- throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ Log.w(TAG, "onReceive " + action);
+ if (action.contentEquals(LocalService.ACTION_CONF_UPDATE)) {
+ confUpdate();
+ } else if (action.contentEquals(DRingService.VIDEO_EVENT)) {
+ if (video == null)
+ return;
+ Conference conf = getConference();
+ if (intent.hasExtra("start")) {
+ Log.w(TAG, "VIDEO_EVENT start");
+ video.setVisibility(View.VISIBLE);
+ videoPreview.setVisibility(View.VISIBLE);
+ } else if (intent.hasExtra("camera")) {
+ Log.w(TAG, "VIDEO_EVENT camera " + intent.getBooleanExtra("camera", false));
+ previewWidth = intent.getIntExtra("width", 0);
+ previewHeight = intent.getIntExtra("height", 0);
+ }
+ else if (conf != null && conf.getId().equals(intent.getStringExtra("call"))) {
+ if (video != null) {
+ haveVideo = intent.getBooleanExtra("started", false);
+ if (haveVideo) {
+ Log.w(TAG, "VIDEO_EVENT started");
+ video.setVisibility(View.VISIBLE);
+ videoPreview.setVisibility(View.VISIBLE);
+ videoWidth = intent.getIntExtra("width", 0);
+ videoHeight = intent.getIntExtra("height", 0);
+ } else {
+ Log.w(TAG, "VIDEO_EVENT stopped");
+ video.setVisibility(View.GONE);
+ videoPreview.setVisibility(View.GONE);
+ }
+ }
+ refreshState();
+ }
+ resetVideoSizes();
+ }
+ else if (action.contentEquals(CallManagerCallBack.RECORD_STATE_CHANGED)) {
+ recordingChanged((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"), intent.getStringExtra("file"));
+ } else if (action.contentEquals(CallManagerCallBack.ZRTP_OFF)) {
+ secureZrtpOff((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"));
+ } else if (action.contentEquals(CallManagerCallBack.ZRTP_ON)) {
+ secureZrtpOn((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"));
+ } else if (action.contentEquals(CallManagerCallBack.DISPLAY_SAS)) {
+ displaySAS((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"));
+ } else if (action.contentEquals(CallManagerCallBack.ZRTP_NEGOTIATION_FAILED)) {
+ zrtpNegotiationFailed((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"));
+ } else if (action.contentEquals(CallManagerCallBack.ZRTP_NOT_SUPPORTED)) {
+ zrtpNotSupported((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"));
+ } else if (action.contentEquals(CallManagerCallBack.RTCP_REPORT_RECEIVED)) {
+ rtcpReportReceived(null, null); // FIXME
+ } else {
+ Log.e(TAG, "Unknown action: " + intent.getAction());
+ }
}
-
- mCallbacks = (Callbacks) activity;
}
+ private final CallReceiver mReceiver = new CallReceiver();
public void refreshState() {
Conference conf = getConference();
+
if (conf == null) {
contactBubbleView.setImageBitmap(null);
contactBubbleTxt.setText("");
@@ -190,6 +307,9 @@
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
+ case android.R.id.home:
+ mCallbacks.terminateCall();
+ break;
case R.id.menuitem_chat:
Intent intent = new Intent()
.setClass(getActivity(), ConversationActivity.class)
@@ -205,19 +325,51 @@
audioManager.setSpeakerphoneOn(!audioManager.isSpeakerphoneOn());
getActivity().invalidateOptionsMenu();
break;
+ case R.id.menuitem_camera_flip:
+ lastVideoSource = !lastVideoSource;
+ try {
+ mCallbacks.getRemoteService().switchInput(getConference().getId(), lastVideoSource);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ item.setIcon(lastVideoSource ? R.drawable.ic_camera_front_white_24dp : R.drawable.ic_camera_rear_white_24dp);
+ break;
}
return true;
}
@Override
- public void onDetach() {
- super.onDetach();
- mCallbacks = sDummyCallbacks;
+ public void onStop() {
+ Log.w(TAG, "onStop()");
+
+ Conference c = getConference();
+ c.resumeVideo = haveVideo;
+
+ DRingService.videoSurfaces.remove(c.getId());
+ DRingService.mCameraPreviewSurface.clear();
+ try {
+ IDRingService service = mCallbacks.getRemoteService();
+ if (service != null) {
+ service.videoSurfaceRemoved(c.getId());
+ service.videoPreviewSurfaceRemoved();
+ }
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+
+ super.onStop();
}
@Override
- public void onStop() {
- super.onStop();
+ public void onStart() {
+ super.onStart();
+ Conference c = getConference();
+ if (c != null && c.resumeVideo) {
+ haveVideo = true;
+ video.setVisibility(View.VISIBLE);
+ videoPreview.setVisibility(View.VISIBLE);
+ c.resumeVideo = false;
+ }
}
@Override
@@ -225,14 +377,14 @@
Log.w(TAG, "onResume()");
super.onResume();
//initializeWiFiListener();
- refreshState();
-
Conference c = getConference();
if (c != null) {
c.mVisible = true;
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getActivity());
notificationManager.cancel(c.notificationId);
}
+
+ refreshState();
}
@Override
@@ -240,9 +392,7 @@
Log.w(TAG, "onPause()");
super.onPause();
//getActivity().unregisterReceiver(wifiReceiver);
- if (mScreenWakeLock != null && mScreenWakeLock.isHeld()) {
- mScreenWakeLock.release();
- }
+
Conference c = getConference();
if (c != null) {
c.mVisible = false;
@@ -266,8 +416,10 @@
String newState = c.getState();
if (c.isOnGoing()) {
+ ongoingCall = true;
initNormalStateDisplay();
} else if (c.isRinging()) {
+ ongoingCall = false;
mCallStatusTxt.setText(newState);
if (c.isIncoming()) {
@@ -283,6 +435,11 @@
}
@Override
+ public void recordingChanged(Conference c, String callID, String filename) {
+
+ }
+
+ @Override
public void secureZrtpOn(Conference updated, String id) {
Log.i(TAG, "secureZrtpOn");
mCallbacks.updateDisplayedConference(updated);
@@ -316,6 +473,11 @@
}
@Override
+ public void rtcpReportReceived(Conference c, HashMap<String, Integer> stats) {
+
+ }
+
+ @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
SipCall transfer;
@@ -345,17 +507,63 @@
break;
case Activity.RESULT_CANCELED:
default:
- initNormalStateDisplay();
+ confUpdate();
break;
}
}
}
+ void resetVideoSizes() {
+ double video_ratio = videoWidth / (double) videoHeight;
+ double screen_ratio = rootView.getWidth() / (double) rootView.getHeight();
+
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) video.getLayoutParams();
+ int oldW = params.width;
+ int oldH = params.height;
+ if (video_ratio >= screen_ratio) {
+ params.width = RelativeLayout.LayoutParams.MATCH_PARENT;
+ params.height = (int)(videoHeight * (double) rootView.getWidth() / (double) videoWidth);
+ } else {
+ params.height = RelativeLayout.LayoutParams.MATCH_PARENT;
+ params.width = (int)(videoWidth * (double) rootView.getHeight() / (double) videoHeight);
+ }
+
+ if (oldW != params.width || oldH != params.height) {
+ Log.w(TAG, "onLayoutChange " + params.width + " x " + params.height);
+ video.setLayoutParams(params);
+ }
+
+ DisplayMetrics metrics = getResources().getDisplayMetrics();
+ FrameLayout.LayoutParams params_preview = (FrameLayout.LayoutParams) videoPreview.getLayoutParams();
+ oldW = params_preview.width;
+ oldH = params_preview.height;
+ double preview_max_dim = Math.max(previewWidth, previewHeight);
+ double preview_ratio = metrics.density * 160. / preview_max_dim;
+ params_preview.width = (int) (previewWidth * preview_ratio);
+ params_preview.height = (int) (previewHeight * preview_ratio);
+ if (oldW != params_preview.width || oldH != params_preview.height) {
+ Log.w(TAG, "onLayoutChange " + params_preview.width + " x " + params_preview.height);
+ videoPreview.setLayoutParams(params_preview);
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ if (videoPreview.getVisibility() == View.VISIBLE) {
+ try {
+ mCallbacks.getRemoteService().videoPreviewSurfaceAdded();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Log.i(TAG, "onCreateView");
- final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.frag_call, container, false);
+ rootView = (ViewGroup) inflater.inflate(R.layout.frag_call, container, false);
+ contactBubbleLayout = rootView.findViewById(R.id.contact_bubble_layout);
contactBubbleView = (ImageView) rootView.findViewById(R.id.contact_bubble);
contactBubbleTxt = (TextView) rootView.findViewById(R.id.contact_bubble_txt);
contactBubbleNumTxt = (TextView) rootView.findViewById(R.id.contact_bubble_num_txt);
@@ -365,14 +573,106 @@
mCallStatusTxt = (TextView) rootView.findViewById(R.id.call_status_txt);
mSecuritySwitch = (ViewSwitcher) rootView.findViewById(R.id.security_switcher);
securityIndicator = rootView.findViewById(R.id.security_indicator);
+
+ video = (SurfaceView)rootView.findViewById(R.id.video_preview_surface);
+ video.getHolder().setFormat(PixelFormat.RGBA_8888);
+ video.getHolder().addCallback(new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ Log.i(TAG, "video surfaceCreated");
+ contactBubbleLayout.setVisibility(View.GONE);
+ Conference c = getConference();
+ DRingService.videoSurfaces.put(c.getId(), new WeakReference<>(holder));
+ try {
+ mCallbacks.getRemoteService().videoSurfaceAdded(c.getId());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ Log.i(TAG, "video surfaceChanged " + format + ", " + width + " x " + height);
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ Log.i(TAG, "video surfaceDestroyed");
+ Conference c = getConference();
+ DRingService.videoSurfaces.remove(c.getId());
+ try {
+ IDRingService service = mCallbacks.getRemoteService();
+ if (service != null)
+ service.videoSurfaceRemoved(c.getId());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+
+ rootView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View parent, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ resetVideoSizes();
+ }
+ });
+ rootView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ boolean ui = (visibility & (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN)) == 0;
+ if (ongoingCall) {
+ hangupButton.setVisibility(ui ? View.VISIBLE : View.GONE);
+ }
+ }
+ });
+
+ videoPreview = (SurfaceView)rootView.findViewById(R.id.camera_preview_surface);
+ videoPreview.getHolder().setFormat(PixelFormat.RGBA_8888);
+ videoPreview.getHolder().addCallback(new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ DRingService.mCameraPreviewSurface = new WeakReference<>(holder);
+ try {
+ mCallbacks.getRemoteService().videoPreviewSurfaceAdded();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ Log.i(TAG, "videoPreview surfaceChanged " + format + ", " + width + " x " + height);
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ if (videoPreview != null && DRingService.mCameraPreviewSurface.get() == holder) {
+ DRingService.mCameraPreviewSurface.clear();
+ }
+ try {
+ IDRingService service = mCallbacks.getRemoteService();
+ if (service != null)
+ service.videoPreviewSurfaceRemoved();
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ videoPreview.setZOrderMediaOverlay(true);
+
return rootView;
}
public Conference getConference() {
- return mCallbacks.getDisplayedConference();
+ Conference c = mCallbacks.getDisplayedConference();
+ if (c != null) {
+ if (mCachedConference != c)
+ mCachedConference = c;
+ return c;
+ }
+ return mCachedConference;
}
-
private void initContactDisplay(final SipCall call) {
CallContact contact = call.getContact();
final String name = contact.getDisplayName();
@@ -384,7 +684,9 @@
contactBubbleNumTxt.setText(call.getNumber());
}
new ContactPictureTask(getActivity(), contactBubbleView, contact).run();
- mCallbacks.getSupportActionBar().setTitle(name);
+ ActionBar ab = mCallbacks.getSupportActionBar();
+ ab.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE);
+ ab.setTitle(name);
}
private void initNormalStateDisplay() {
@@ -410,6 +712,8 @@
}
});
+ contactBubbleLayout.setVisibility(haveVideo ? View.GONE : View.VISIBLE);
+
updateSecurityDisplay();
}
@@ -466,26 +770,24 @@
mSecuritySwitch.setVisibility(View.VISIBLE);
}
- protected Bitmap getContactPhoto(CallContact contact, int size) {
+ /*protected Bitmap getContactPhoto(CallContact contact, int size) {
if (contact.getPhotoId() > 0) {
return ContactPictureTask.loadContactPhoto(getActivity().getContentResolver(), contact.getId());
} else {
return ContactPictureTask.decodeSampledBitmapFromResource(getResources(), R.drawable.ic_contact_picture, size, size);
}
- }
+ }*/
private void initIncomingCallDisplay() {
Log.i(TAG, "Start incoming display");
- if (mCallbacks.getService().getAccount(getConference().getParticipants().get(0).getAccount()).isAutoanswerEnabled()) {
+ final SipCall call = getConference().getParticipants().get(0);
+ if (mCallbacks.getService().getAccount(call.getAccount()).isAutoanswerEnabled()) {
try {
- mCallbacks.getRemoteService().accept(getConference().getParticipants().get(0).getCallId());
- } catch (RemoteException e) {
- e.printStackTrace();
- } catch (NullPointerException e) {
+ mCallbacks.getRemoteService().accept(call.getCallId());
+ } catch (Exception e) {
e.printStackTrace();
}
} else {
- final SipCall call = getConference().getParticipants().get(0);
initContactDisplay(call);
acceptButton.setVisibility(View.VISIBLE);
acceptButton.setOnClickListener(new OnClickListener() {
@@ -583,9 +885,7 @@
mCallbacks.getRemoteService().playDtmf(toSend);
break;
}
- } catch (RemoteException e) {
- e.printStackTrace();
- } catch (NullPointerException e) {
+ } catch (Exception e) {
e.printStackTrace();
}
}
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/CallListFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/CallListFragment.java
index 0bc6b8a..ba63dd0 100644
--- a/ring-android/app/src/main/java/cx/ring/fragments/CallListFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/fragments/CallListFragment.java
@@ -148,9 +148,10 @@
}
public void updateLists() {
- if (mCallbacks.getService() != null && mConferenceAdapter != null) {
- mConferenceAdapter.updateDataset(mCallbacks.getService().getConversations());
- if (mCallbacks.getService().isConnected()) {
+ LocalService service = mCallbacks.getService();
+ if (service != null && mConferenceAdapter != null) {
+ mConferenceAdapter.updateDataset(service.getConversations());
+ if (service.isConnected()) {
error_msg_pane.setVisibility(View.GONE);
} else {
error_msg_pane.setVisibility(View.VISIBLE);
@@ -354,7 +355,7 @@
mListAdapter = new ContactsAdapter(getActivity(), (HomeActivity)getActivity(), service.get40dpContactCache(), service.getThreadPool());
mGridAdapter = new StarredContactsAdapter(getActivity());
- mConferenceAdapter.updateDataset(mCallbacks.getService().getConversations());
+ mConferenceAdapter.updateDataset(service.getConversations());
list.setAdapter(mConferenceAdapter);
return inflatedView;
@@ -393,7 +394,10 @@
Uri baseUri = null;
if (args != null)
baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, Uri.encode(args.getString("filter")));
- ContactsLoader l = new ContactsLoader(getActivity(), baseUri, mCallbacks.getService().getContactCache());
+ LocalService service = mCallbacks.getService();
+ if (service == null)
+ return null;
+ ContactsLoader l = new ContactsLoader(getActivity(), baseUri, service.getContactCache());
l.forceLoad();
return l;
}
diff --git a/ring-android/app/src/main/java/cx/ring/fragments/CallableWrapperFragment.java b/ring-android/app/src/main/java/cx/ring/fragments/CallableWrapperFragment.java
deleted file mode 100644
index 8d21157..0000000
--- a/ring-android/app/src/main/java/cx/ring/fragments/CallableWrapperFragment.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2004-2016 Savoir-faire Linux Inc.
- *
- * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
- * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
- */
-
-package cx.ring.fragments;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.app.Fragment;
-import android.util.Log;
-import cx.ring.interfaces.CallInterface;
-import cx.ring.model.Conference;
-import cx.ring.service.CallManagerCallBack;
-import cx.ring.service.LocalService;
-
-import java.util.HashMap;
-
-public abstract class CallableWrapperFragment extends Fragment implements CallInterface
-{
- private final CallReceiver mReceiver = new CallReceiver();
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(CallManagerCallBack.RECORD_STATE_CHANGED);
- intentFilter.addAction(CallManagerCallBack.ZRTP_OFF);
- intentFilter.addAction(CallManagerCallBack.ZRTP_ON);
- intentFilter.addAction(CallManagerCallBack.DISPLAY_SAS);
- intentFilter.addAction(CallManagerCallBack.ZRTP_NEGOTIATION_FAILED);
- intentFilter.addAction(CallManagerCallBack.ZRTP_NOT_SUPPORTED);
- intentFilter.addAction(CallManagerCallBack.RTCP_REPORT_RECEIVED);
-
- intentFilter.addAction(LocalService.ACTION_CONF_UPDATE);
-
- getActivity().registerReceiver(mReceiver, intentFilter);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- getActivity().unregisterReceiver(mReceiver);
- }
-
- @Override
- public void confUpdate() {
- }
-
- @Override
- public void recordingChanged(Conference c, String callID, String filename) {
- }
-
- @Override
- public void secureZrtpOn(Conference c, String id) {
- }
-
- @Override
- public void secureZrtpOff(Conference c, String id) {
- }
-
- @Override
- public void displaySAS(Conference c, String securedCallID) {
- }
-
- @Override
- public void zrtpNegotiationFailed(Conference c, String securedCallID) {
- }
-
- @Override
- public void zrtpNotSupported(Conference c, String securedCallID) {
- }
-
- @Override
- public void rtcpReportReceived(Conference c, HashMap<String, Integer> stats) {
- }
-
-
- public class CallReceiver extends BroadcastReceiver {
- private final String TAG = CallReceiver.class.getSimpleName();
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if(intent.getAction().contentEquals(LocalService.ACTION_CONF_UPDATE)) {
- confUpdate();
- } else if (intent.getAction().contentEquals(CallManagerCallBack.RECORD_STATE_CHANGED)) {
- recordingChanged((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"), intent.getStringExtra("file"));
- } else if (intent.getAction().contentEquals(CallManagerCallBack.ZRTP_OFF)) {
- secureZrtpOff((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"));
- } else if (intent.getAction().contentEquals(CallManagerCallBack.ZRTP_ON)) {
- secureZrtpOn((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"));
- } else if (intent.getAction().contentEquals(CallManagerCallBack.DISPLAY_SAS)) {
- displaySAS((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"));
- } else if (intent.getAction().contentEquals(CallManagerCallBack.ZRTP_NEGOTIATION_FAILED)) {
- zrtpNegotiationFailed((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"));
- } else if (intent.getAction().contentEquals(CallManagerCallBack.ZRTP_NOT_SUPPORTED)) {
- zrtpNotSupported((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"));
- } else if (intent.getAction().contentEquals(CallManagerCallBack.RTCP_REPORT_RECEIVED)) {
- rtcpReportReceived(null, null); // FIXME
- } else {
- Log.e(TAG, "Unknown action: " + intent.getAction());
- }
-
- }
-
- }
-}
diff --git a/ring-android/app/src/main/java/cx/ring/model/Conference.java b/ring-android/app/src/main/java/cx/ring/model/Conference.java
index c851511..32a8d76 100644
--- a/ring-android/app/src/main/java/cx/ring/model/Conference.java
+++ b/ring-android/app/src/main/java/cx/ring/model/Conference.java
@@ -50,6 +50,7 @@
public int notificationId;
// true if this conference is currently presented to the user.
public boolean mVisible = false;
+ public boolean resumeVideo = false;
private final static Random rand = new Random();
diff --git a/ring-android/app/src/main/java/cx/ring/model/Conversation.java b/ring-android/app/src/main/java/cx/ring/model/Conversation.java
index fbb426a..12b738c 100644
--- a/ring-android/app/src/main/java/cx/ring/model/Conversation.java
+++ b/ring-android/app/src/main/java/cx/ring/model/Conversation.java
@@ -23,6 +23,7 @@
import android.content.res.Resources;
import android.database.ContentObservable;
import android.support.v4.app.NotificationCompat;
+import android.util.Log;
import android.util.Pair;
import java.util.ArrayList;
@@ -203,6 +204,7 @@
last = e.getKey();
}
}
+ Log.i(TAG, "getLastAccountUsed " + last);
return last;
}
diff --git a/ring-android/app/src/main/java/cx/ring/model/SipCall.java b/ring-android/app/src/main/java/cx/ring/model/SipCall.java
index 9a68bae..1308aa3 100644
--- a/ring-android/app/src/main/java/cx/ring/model/SipCall.java
+++ b/ring-android/app/src/main/java/cx/ring/model/SipCall.java
@@ -47,6 +47,8 @@
private int mCallType;
private int mCallState = State.NONE;
+ private String videoSource = null;
+
public SipCall(String id, String account, SipUri number, int direction) {
mCallID = id;
mAccount = account;
@@ -83,8 +85,7 @@
call_details.get("PEER_NUMBER"),
Integer.parseInt(call_details.get("CALL_TYPE")));
mCallState = stateFromString(call_details.get("CALL_STATE"));
- isPeerHolding = call_details.get("PEER_HOLDING").contentEquals("true");
- isAudioMuted = call_details.get("AUDIO_MUTED").contentEquals("true");
+ setDetails(call_details);
}
public String getRecordPath() {
@@ -100,15 +101,20 @@
return mCallState;
}
- public void setDetails(HashMap<String, String> details) {
+ public void setDetails(Map<String, String> details) {
isPeerHolding = "true".equals(details.get("PEER_HOLDING"));
isAudioMuted = "true".equals(details.get("AUDIO_MUTED"));
+ videoSource = details.get("VIDEO_SOURCE");
}
public long getDuration() {
return isMissed() ? 0 : timestampEnd - timestampStart;
}
+ public String getVideoSource() {
+ return videoSource;
+ }
+
public interface Direction {
int INCOMING = 0;
int OUTGOING = 1;
diff --git a/ring-android/app/src/main/java/cx/ring/model/account/AccountDetailBasic.java b/ring-android/app/src/main/java/cx/ring/model/account/AccountDetailBasic.java
index 75708b6..04910f7 100644
--- a/ring-android/app/src/main/java/cx/ring/model/account/AccountDetailBasic.java
+++ b/ring-android/app/src/main/java/cx/ring/model/account/AccountDetailBasic.java
@@ -98,7 +98,7 @@
for (AccountDetail.PreferenceEntry p : privateArray) {
map.put(p.mKey, p.mValue);
}
- map.put(AccountDetailBasic.CONFIG_VIDEO_ENABLED, "false");
+ map.put(AccountDetailBasic.CONFIG_VIDEO_ENABLED, "true");
return map;
}
diff --git a/ring-android/app/src/main/java/cx/ring/service/ConfigurationManagerCallback.java b/ring-android/app/src/main/java/cx/ring/service/ConfigurationManagerCallback.java
index a3ec767..63b78a5 100644
--- a/ring-android/app/src/main/java/cx/ring/service/ConfigurationManagerCallback.java
+++ b/ring-android/app/src/main/java/cx/ring/service/ConfigurationManagerCallback.java
@@ -31,13 +31,14 @@
public class ConfigurationManagerCallback extends ConfigurationCallback {
- private DRingService mService;
- private static final String TAG = "ConfigurationManagerCb";
+ private static final String TAG = ConfigurationManagerCallback.class.getSimpleName();
static public final String ACCOUNTS_CHANGED = "accounts-changed";
static public final String ACCOUNT_STATE_CHANGED = "account-State-changed";
static public final String INCOMING_TEXT = "incoming--txt-msg";
+ private final DRingService mService;
+
public ConfigurationManagerCallback(DRingService context) {
super();
mService = context;
diff --git a/ring-android/app/src/main/java/cx/ring/service/DRingService.java b/ring-android/app/src/main/java/cx/ring/service/DRingService.java
index c9c2108..003d53f 100644
--- a/ring-android/app/src/main/java/cx/ring/service/DRingService.java
+++ b/ring-android/app/src/main/java/cx/ring/service/DRingService.java
@@ -28,10 +28,14 @@
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.IntentFilter;
+import android.hardware.Camera;
import android.media.AudioManager;
import android.os.Handler;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -42,6 +46,11 @@
import android.util.Log;
import cx.ring.BuildConfig;
+
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.WindowManager;
+
import cx.ring.model.Codec;
import cx.ring.utils.SwigNativeConverter;
@@ -53,6 +62,7 @@
private static HandlerThread executorThread;
static public final String DRING_CONNECTION_CHANGED = BuildConfig.APPLICATION_ID + ".event.DRING_CONNECTION_CHANGE";
+ static public final String VIDEO_EVENT = BuildConfig.APPLICATION_ID + ".event.VIDEO_EVENT";
private Handler handler = new Handler();
private static int POLLING_TIMEOUT = 50;
@@ -72,6 +82,21 @@
private ConfigurationManagerCallback configurationCallback;
private CallManagerCallBack callManagerCallBack;
+ private VideoManagerCallback videoManagerCallback;
+
+ class Shm {
+ String id;
+ String path;
+ int w, h;
+ boolean mixer;
+ long window = 0;
+ };
+
+ static public WeakReference<SurfaceHolder> mCameraPreviewSurface = new WeakReference<>(null);
+ static public Map<String, WeakReference<SurfaceHolder>> videoSurfaces = Collections.synchronizedMap(new HashMap<String, WeakReference<SurfaceHolder>>());
+ private final Map<String, Shm> videoInputs = new HashMap<>();
+ private Camera previewCamera = null;
+ private VideoParams previewParams = null;
static private final IntentFilter RINGER_FILTER = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION);
private final BroadcastReceiver ringerModeListener = new BroadcastReceiver() {
@@ -147,6 +172,202 @@
return mExecutor;
}
+ public void decodingStarted(String id, String shm_path, int w, int h, boolean is_mixer) {
+ Log.i(TAG, "DRingService.decodingStarted() " + id + " " + w + "x" + h);
+ Shm shm = new Shm();
+ shm.id = id;
+ shm.path = shm_path;
+ shm.w = w;
+ shm.h = h;
+ shm.mixer = is_mixer;
+ videoInputs.put(id, shm);
+ WeakReference<SurfaceHolder> w_holder = videoSurfaces.get(id);
+ if (w_holder != null) {
+ SurfaceHolder holder = w_holder.get();
+ if (holder != null)
+ startVideo(shm, holder);
+ }
+ }
+
+ public void decodingStopped(String id) {
+ Log.i(TAG, "DRingService.decodingStopped() " + id);
+ Shm shm = videoInputs.remove(id);
+ if (shm != null)
+ stopVideo(shm);
+ }
+
+ private void startVideo(Shm input, SurfaceHolder holder) {
+ Log.i(TAG, "DRingService.startVideo() " + input.id);
+ input.window = RingserviceJNI.acquireNativeWindow(holder.getSurface());
+ if (input.window == 0) {
+ Log.i(TAG, "DRingService.startVideo() no window ! " + input.id);
+ Intent intent = new Intent(VIDEO_EVENT);
+ intent.putExtra("start", true);
+ sendBroadcast(intent);
+ return;
+ }
+ RingserviceJNI.setNativeWindowGeometry(input.window, input.w, input.h);
+ RingserviceJNI.registerVideoCallback(input.id, input.window);
+
+ Intent intent = new Intent(VIDEO_EVENT);
+ intent.putExtra("started", true);
+ intent.putExtra("call", input.id);
+ intent.putExtra("width", input.w);
+ intent.putExtra("height", input.h);
+ sendBroadcast(intent);
+ }
+
+ private void stopVideo(Shm input) {
+ Log.i(TAG, "DRingService.stopVideo() " + input.id);
+ if (input.window != 0) {
+ RingserviceJNI.unregisterVideoCallback(input.id, input.window);
+ RingserviceJNI.releaseNativeWindow(input.window);
+ input.window = 0;
+ }
+
+ Intent intent = new Intent(VIDEO_EVENT);
+ intent.putExtra("started", false);
+ intent.putExtra("call", input.id);
+ sendBroadcast(intent);
+ }
+
+ static public class VideoParams {
+ public VideoParams(int id, int format, int width, int height, int rate) {
+ this.id = id;
+ this.format = format;
+ this.width = width;
+ this.height = height;
+ this.rate = rate;
+ }
+
+ public int id;
+ public int format;
+ public int width;
+ public int height;
+ public int rate;
+ public int rotation;
+ }
+
+ public int setCameraDisplayOrientation(int cameraId, android.hardware.Camera camera) {
+ android.hardware.Camera.CameraInfo info =
+ new android.hardware.Camera.CameraInfo();
+ android.hardware.Camera.getCameraInfo(cameraId, info);
+ WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
+ int rotation = windowManager.getDefaultDisplay().getRotation();
+ int degrees = 0;
+ switch (rotation) {
+ case Surface.ROTATION_0: degrees = 0; break;
+ case Surface.ROTATION_90: degrees = 90; break;
+ case Surface.ROTATION_180: degrees = 180; break;
+ case Surface.ROTATION_270: degrees = 270; break;
+ }
+
+ int result;
+ if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ result = (info.orientation + degrees) % 360;
+ result = (360 - result) % 360; // compensate the mirror
+ } else { // back-facing
+ result = (info.orientation - degrees + 360) % 360;
+ }
+ camera.setDisplayOrientation(result);
+ return result;
+ }
+
+ public void startCapture(VideoParams p) {
+ stopCapture();
+
+ SurfaceHolder surface = mCameraPreviewSurface.get();
+ if (surface == null) {
+ Log.w(TAG, "Can't start capture: no surface registered.");
+ previewParams = p;
+ Intent intent = new Intent(VIDEO_EVENT);
+ intent.putExtra("start", true);
+ sendBroadcast(intent);
+ return;
+ }
+
+ if (p == null) {
+ Log.w(TAG, "startCapture: no video parameters ");
+ return;
+ }
+ Log.w(TAG, "startCapture " + p.id);
+
+ final Camera preview;
+ try {
+ preview = Camera.open(p.id);
+ p.rotation = setCameraDisplayOrientation(p.id, preview);
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage());
+ return;
+ }
+
+ try {
+ surface.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+ preview.setPreviewDisplay(surface);
+ } catch (IOException e) {
+ Log.e(TAG, e.getMessage());
+ return;
+ }
+
+ Camera.Parameters parameters = preview.getParameters();
+ parameters.setPreviewFormat(p.format);
+ parameters.setPreviewSize(p.width, p.height);
+ for (int[] fps : parameters.getSupportedPreviewFpsRange()) {
+ if (p.rate >= fps[Camera.Parameters.PREVIEW_FPS_MIN_INDEX] &&
+ p.rate <= fps[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]) {
+ parameters.setPreviewFpsRange(fps[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
+ fps[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
+ }
+ }
+
+ try {
+ preview.setParameters(parameters);
+ } catch (RuntimeException e) {
+ Log.e(TAG, e.getMessage());
+ }
+
+ preview.setPreviewCallback(videoManagerCallback);
+ preview.setErrorCallback(new Camera.ErrorCallback() {
+ @Override
+ public void onError(int error, Camera cam) {
+ Log.w(TAG, "Camera onError " + error);
+ if (preview == cam)
+ stopCapture();
+ }
+ });
+ preview.startPreview();
+
+ previewCamera = preview;
+ previewParams = p;
+
+ Intent intent = new Intent(VIDEO_EVENT);
+ intent.putExtra("camera", p.id == 1);
+ intent.putExtra("started", true);
+ boolean invert = p.rotation == 90 || p.rotation == 270;
+ intent.putExtra("width", invert ? p.height : p.width);
+ intent.putExtra("height", invert ? p.width : p.height);
+ sendBroadcast(intent);
+ }
+
+ public void stopCapture() {
+ Log.w(TAG, "stopCapture " + previewCamera);
+ if (previewCamera != null) {
+ final Camera preview = previewCamera;
+ final VideoParams p = previewParams;
+ previewCamera = null;
+ preview.setPreviewCallback(null);
+ preview.stopPreview();
+ preview.release();
+
+ Intent intent = new Intent(VIDEO_EVENT);
+ intent.putExtra("camera", p.id == 1);
+ intent.putExtra("started", false);
+ intent.putExtra("width", p.width);
+ intent.putExtra("height", p.height);
+ sendBroadcast(intent);
+ }
+ }
+
// Executes immediate tasks in a single executorThread.
public static class SipServiceExecutor extends Handler {
@@ -263,6 +484,9 @@
handler.removeCallbacks(pollEvents);
if (isPjSipStackStarted) {
Ringservice.fini();
+ configurationCallback = null;
+ callManagerCallBack = null;
+ videoManagerCallback = null;
isPjSipStackStarted = false;
Log.i(TAG, "PjSIPStack stopped");
Intent intent = new Intent(DRING_CONNECTION_CHANGED);
@@ -290,7 +514,8 @@
configurationCallback = new ConfigurationManagerCallback(this);
callManagerCallBack = new CallManagerCallBack(this);
- Ringservice.init(configurationCallback, callManagerCallBack);
+ videoManagerCallback = new VideoManagerCallback(this);
+ Ringservice.init(configurationCallback, callManagerCallBack, videoManagerCallback);
ringerModeChanged(((AudioManager) getSystemService(Context.AUDIO_SERVICE)).getRingerMode());
registerReceiver(ringerModeListener, RINGER_FILTER);
@@ -300,6 +525,8 @@
Intent intent = new Intent(DRING_CONNECTION_CHANGED);
intent.putExtra("connected", isPjSipStackStarted);
sendBroadcast(intent);
+
+ videoManagerCallback.init();
}
// Enforce same thread contract to ensure we do not call from somewhere else
@@ -1181,5 +1408,43 @@
});
}
+ public void videoSurfaceAdded(String id)
+ {
+ Log.i(TAG, "DRingService.videoSurfaceAdded() " + id);
+ Shm shm = videoInputs.get(id);
+ SurfaceHolder holder = videoSurfaces.get(id).get();
+ if (shm != null && holder != null && shm.window == 0)
+ startVideo(shm, holder);
+ }
+
+ public void videoSurfaceRemoved(String id)
+ {
+ Log.i(TAG, "DRingService.videoSurfaceRemoved() " + id);
+ Shm shm = videoInputs.get(id);
+ if (shm != null)
+ stopVideo(shm);
+ }
+
+ public void videoPreviewSurfaceAdded() {
+ Log.i(TAG, "DRingService.videoPreviewSurfaceChanged()");
+ startCapture(previewParams);
+ }
+
+ public void videoPreviewSurfaceRemoved() {
+ Log.i(TAG, "DRingService.videoPreviewSurfaceChanged()");
+ stopCapture();
+ }
+
+ public void switchInput(final String id, final boolean front) {
+ getExecutor().execute(new SipRunnable() {
+ @Override
+ protected void doRun() throws SameThreadException, RemoteException {
+ String uri = "camera://" + (front ? videoManagerCallback.cameraFront : videoManagerCallback.cameraBack);
+ Log.i(TAG, "DRingService.switchInput() " + uri);
+ Ringservice.switchInput(id, uri);
+ }
+ });
+ }
+
};
}
diff --git a/ring-android/app/src/main/java/cx/ring/service/IDRingService.aidl b/ring-android/app/src/main/java/cx/ring/service/IDRingService.aidl
index 2faa8db..20bf9d3 100644
--- a/ring-android/app/src/main/java/cx/ring/service/IDRingService.aidl
+++ b/ring-android/app/src/main/java/cx/ring/service/IDRingService.aidl
@@ -80,7 +80,14 @@
void transfer(in String callID, in String to);
void attendedTransfer(in String transferID, in String targetID);
-
+
+ /* Video */
+ void switchInput(in String call, in boolean front);
+ void videoSurfaceAdded(in String call);
+ void videoSurfaceRemoved(in String call);
+ void videoPreviewSurfaceAdded();
+ void videoPreviewSurfaceRemoved();
+
/* Conference related methods */
void removeConference(in String confID);
diff --git a/ring-android/app/src/main/java/cx/ring/service/LocalService.java b/ring-android/app/src/main/java/cx/ring/service/LocalService.java
index e4e3735..a76f58c 100644
--- a/ring-android/app/src/main/java/cx/ring/service/LocalService.java
+++ b/ring-android/app/src/main/java/cx/ring/service/LocalService.java
@@ -115,7 +115,7 @@
public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
public static final int PERMISSIONS_REQUEST = 57;
- public final static String[] REQUIRED_RUNTIME_PERMISSIONS = {Manifest.permission.RECORD_AUDIO};
+ public final static String[] REQUIRED_RUNTIME_PERMISSIONS = {Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA};
private IDRingService mService = null;
private final ContactsContentObserver contactContentObserver = new ContactsContentObserver();
diff --git a/ring-android/app/src/main/java/cx/ring/service/VideoManagerCallback.java b/ring-android/app/src/main/java/cx/ring/service/VideoManagerCallback.java
new file mode 100644
index 0000000..731cc31
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/service/VideoManagerCallback.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2015-2016 Savoir-faire Linux Inc.
+ *
+ * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Damien Riegel <damien.riegel@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.service;
+
+import android.hardware.Camera;
+import android.util.Log;
+import android.view.SurfaceHolder;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+
+public class VideoManagerCallback extends VideoCallback implements Camera.PreviewCallback
+{
+ private static final String TAG = VideoManagerCallback.class.getSimpleName();
+
+ private final DRingService mService;
+ private final HashMap<String, DRingService.VideoParams> params = new HashMap<>();
+
+ public int cameraFront = 0;
+ public int cameraBack = 0;
+
+ public VideoManagerCallback(DRingService s) {
+ mService = s;
+ }
+
+ public void init() {
+ int number_cameras = getNumberOfCameras();
+ Camera.CameraInfo camInfo = new Camera.CameraInfo();
+ for(int i = 0; i < number_cameras; i++) {
+ RingserviceJNI.addVideoDevice(Integer.toString(i));
+ Camera.getCameraInfo(i, camInfo);
+ if (camInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)
+ cameraFront = i;
+ else
+ cameraBack = i;
+ Log.d(TAG, "Camera number " + i);
+ }
+ RingserviceJNI.setDefaultDevice(Integer.toString(cameraFront));
+ }
+
+ @Override
+ public void decodingStarted(String id, String shm_path, int w, int h, boolean is_mixer) {
+ mService.decodingStarted(id, shm_path, w, h, is_mixer);
+ }
+
+ @Override
+ public void decodingStopped(String id, String shm_path, boolean is_mixer) {
+ mService.decodingStopped(id);
+ }
+
+ public void onPreviewFrame(byte[] data, Camera camera) {
+ int ptr = RingserviceJNI.obtainFrame(data.length);
+ if (ptr != 0)
+ RingserviceJNI.setVideoFrame(data, data.length, ptr);
+ RingserviceJNI.releaseFrame(ptr);
+ }
+
+ public void setParameters(String camid, int format, int width, int height, int rate) {
+ int id = Integer.valueOf(camid);
+ DRingService.VideoParams p = new DRingService.VideoParams(id, format, width, height, rate);
+ params.put(camid, p);
+ }
+
+ public void startCapture(String camid) {
+ DRingService.VideoParams p = params.get(camid);
+ if (p == null)
+ return;
+
+ mService.startCapture(p);
+ }
+
+ public void stopCapture() {
+ mService.stopCapture();
+ }
+
+ @Override
+ public void getCameraInfo(String camid, IntVect formats, UintVect sizes, UintVect rates) {
+ int id = Integer.valueOf(camid);
+
+ if (id < 0 || id >= getNumberOfCameras())
+ return;
+
+ Camera cam;
+ try {
+ cam = Camera.open(id);
+ } catch (Exception e) {
+ Log.d(TAG, e.getMessage());
+ return;
+ }
+
+ Camera.CameraInfo camInfo = new Camera.CameraInfo();
+ Camera.getCameraInfo(id, camInfo);
+
+ Camera.Parameters param = cam.getParameters();
+ cam.release();
+
+ getFormats(param, formats);
+ getSizes(param, sizes);
+ getRates(param, rates);
+ }
+
+ private int getNumberOfCameras() {
+ return Camera.getNumberOfCameras();
+ }
+
+ private void getFormats(Camera.Parameters param, IntVect formats_) {
+ for(int fmt : param.getSupportedPreviewFormats()) {
+ formats_.add(fmt);
+ }
+ }
+
+ private void getSizes(Camera.Parameters param, UintVect sizes) {
+ int sw = 1280, sh = 720;
+ for(Camera.Size s : param.getSupportedPreviewSizes()) {
+ if (s.width < sw) {
+ sw = s.width;
+ sh = s.height;
+ }
+ }
+ Log.d(TAG, "Supported size: " + sw + " x " + sh);
+ sizes.add(sw);
+ sizes.add(sh);
+ }
+
+ private void getRates(Camera.Parameters param, UintVect rates_) {
+ for(int fps[] : param.getSupportedPreviewFpsRange()) {
+ int rate = (fps[Camera.Parameters.PREVIEW_FPS_MIN_INDEX] + fps[Camera.Parameters.PREVIEW_FPS_MAX_INDEX])/2;
+ rates_.add(rate);
+ }
+ }
+}
diff --git a/ring-android/app/src/main/java/cx/ring/views/FitsWindowsLayout.java b/ring-android/app/src/main/java/cx/ring/views/FitsWindowsLayout.java
new file mode 100644
index 0000000..fec3d8d
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/views/FitsWindowsLayout.java
@@ -0,0 +1,40 @@
+package cx.ring.views;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+public class FitsWindowsLayout extends FrameLayout {
+
+ private Rect windowInsets = new Rect();
+ private Rect tempInsets = new Rect();
+
+ public FitsWindowsLayout(Context context) {
+ super(context);
+ }
+
+ public FitsWindowsLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public FitsWindowsLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected boolean fitSystemWindows(Rect insets) {
+ windowInsets.set(insets);
+ super.fitSystemWindows(insets);
+ return false;
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ super.addView(child, index, params);
+ tempInsets.set(windowInsets);
+ super.fitSystemWindows(tempInsets);
+ }
+}
\ No newline at end of file
diff --git a/ring-android/app/src/main/jni/Android.mk b/ring-android/app/src/main/jni/Android.mk
index 7e190b6..d935c67 100644
--- a/ring-android/app/src/main/jni/Android.mk
+++ b/ring-android/app/src/main/jni/Android.mk
@@ -127,6 +127,7 @@
-lgmp -lgnutls -lopendht \
-lavformat -lavcodec -lavutil \
-lopus -lspeex \
+ -landroid \
$(CPP_STATIC)
diff --git a/ring-android/app/src/main/jni/jni_interface.i b/ring-android/app/src/main/jni/jni_interface.i
index 3205a23..0e28150 100644
--- a/ring-android/app/src/main/jni/jni_interface.i
+++ b/ring-android/app/src/main/jni/jni_interface.i
@@ -151,6 +151,7 @@
}
%}
%template(Blob) vector<uint8_t>;
+%template(FloatVect) vector<float>;
}
/* not parsed by SWIG but needed by generated C files */
@@ -167,6 +168,7 @@
%include "managerimpl.i"
%include "callmanager.i"
%include "configurationmanager.i"
+%include "videomanager.i"
#include "dring/callmanager_interface.h"
@@ -175,13 +177,14 @@
* that are not declared elsewhere in the c++ code
*/
-void init(ConfigurationCallback* confM, Callback* callM) {
+void init(ConfigurationCallback* confM, Callback* callM, VideoCallback* videoM) {
using namespace std::placeholders;
using std::bind;
using DRing::exportable_callback;
using DRing::CallSignal;
using DRing::ConfigurationSignal;
+ using DRing::VideoSignal;
using SharedCallback = std::shared_ptr<DRing::CallbackWrapperBase>;
@@ -249,16 +252,23 @@
#endif
*/
+ const std::map<std::string, SharedCallback> videoEvHandlers = {
+ exportable_callback<VideoSignal::GetCameraInfo>(bind(&VideoCallback::getCameraInfo, videoM, _1, _2, _3, _4)),
+ exportable_callback<VideoSignal::SetParameters>(bind(&VideoCallback::setParameters, videoM, _1, _2, _3, _4, _5)),
+ exportable_callback<VideoSignal::StartCapture>(bind(&VideoCallback::startCapture, videoM, _1)),
+ exportable_callback<VideoSignal::StopCapture>(bind(&VideoCallback::stopCapture, videoM)),
+ exportable_callback<VideoSignal::DecodingStarted>(bind(&VideoCallback::decodingStarted, videoM, _1, _2, _3, _4, _5)),
+ exportable_callback<VideoSignal::DecodingStopped>(bind(&VideoCallback::decodingStopped, videoM, _1, _2, _3)),
+ };
+
if (!DRing::init(static_cast<DRing::InitFlag>(DRing::DRING_FLAG_DEBUG)))
return -1;
registerCallHandlers(callEvHandlers);
registerConfHandlers(configEvHandlers);
-/* registerPresHandlers(presEvHandlers);
-#ifdef RING_VIDEO
+/* registerPresHandlers(presEvHandlers); */
registerVideoHandlers(videoEvHandlers);
-#endif
-*/
+
DRing::start();
}
diff --git a/ring-android/app/src/main/jni/videomanager.i b/ring-android/app/src/main/jni/videomanager.i
new file mode 100644
index 0000000..a40d527
--- /dev/null
+++ b/ring-android/app/src/main/jni/videomanager.i
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2015-2016 Savoir-faire Linux Inc.
+ *
+ * Authors: Damien Riegel <damien.riegel@savoirfairelinux.com>
+ * Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ * Ciro Santilli <ciro.santilli@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+%include <std_shared_ptr.i>
+%header %{
+#include <functional>
+#include <list>
+#include <mutex>
+
+#include "dring/dring.h"
+#include "dring/videomanager_interface.h"
+#include <android/native_window.h>
+#include <android/native_window_jni.h>
+#include <android/log.h>
+
+class VideoCallback {
+public:
+ virtual ~VideoCallback(){}
+ virtual void getCameraInfo(const std::string& device, std::vector<int> *formats, std::vector<unsigned> *sizes, std::vector<unsigned> *rates) {}
+ virtual void setParameters(const std::string, const int format, const int width, const int height, const int rate) {}
+ virtual void startCapture(const std::string& camid) {}
+ virtual void stopCapture() {}
+ virtual void decodingStarted(const std::string& id, const std::string& shm_path, int w, int h, bool is_mixer) {}
+ virtual void decodingStopped(const std::string& id, const std::string& shm_path, bool is_mixer) {}
+};
+%}
+
+%feature("director") VideoCallback;
+
+%{
+
+std::map<ANativeWindow*, std::unique_ptr<DRing::FrameBuffer>> windows {};
+std::mutex windows_mutex;
+
+JNIEXPORT void JNICALL Java_cx_ring_service_RingserviceJNI_setVideoFrame(JNIEnv *jenv, jclass jcls, void * jarg1, jint jarg2, jlong jarg3)
+{
+ jenv->GetByteArrayRegion(jarg1, 0, jarg2, jarg3);
+}
+
+JNIEXPORT jlong JNICALL Java_cx_ring_service_RingserviceJNI_acquireNativeWindow(JNIEnv *jenv, jclass jcls, jobject javaSurface)
+{
+ return (jlong)(intptr_t)ANativeWindow_fromSurface(jenv, javaSurface);
+}
+
+JNIEXPORT jlong JNICALL Java_cx_ring_service_RingserviceJNI_releaseNativeWindow(JNIEnv *jenv, jclass jcls, jlong window_)
+{
+ std::lock_guard<std::mutex> guard(windows_mutex);
+ ANativeWindow *window = (ANativeWindow*)((intptr_t) window_);
+ ANativeWindow_release(window);
+}
+
+JNIEXPORT void JNICALL Java_cx_ring_service_RingserviceJNI_setNativeWindowGeometry(JNIEnv *jenv, jclass jcls, jlong window_, int width, int height)
+{
+ ANativeWindow *window = (ANativeWindow*)((intptr_t) window_);
+ ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);
+}
+
+void AndroidDisplayCb(ANativeWindow *window, std::unique_ptr<DRing::FrameBuffer> frame)
+{
+ std::lock_guard<std::mutex> guard(windows_mutex);
+ try {
+ auto& i = windows.at(window);
+ ANativeWindow_Buffer buffer;
+ if (ANativeWindow_lock(window, &buffer, NULL) == 0) {
+ if (buffer.bits && frame && frame->ptr) {
+ if (buffer.stride == frame->width)
+ memcpy(buffer.bits, frame->ptr, frame->width * frame->height * 4);
+ else {
+ size_t line_size_in = frame->width * 4;
+ size_t line_size_out = buffer.stride * 4;
+ for (size_t i=0, n=frame->height; i<n; i++)
+ memcpy(buffer.bits + line_size_out * i, frame->ptr + line_size_in * i, line_size_in);
+ }
+ }
+ else
+ __android_log_print(ANDROID_LOG_WARN, "videomanager.i", "Can't copy surface");
+ ANativeWindow_unlockAndPost(window);
+ } else {
+ __android_log_print(ANDROID_LOG_WARN, "videomanager.i", "Can't lock surface");
+ }
+ i = std::move(frame);
+ } catch (...) {
+ __android_log_print(ANDROID_LOG_WARN, "videomanager.i", "Can't copy frame: no window");
+ }
+}
+
+std::unique_ptr<DRing::FrameBuffer> sinkTargetPullCallback(ANativeWindow *window, std::size_t bytes)
+{
+ try {
+ std::unique_ptr<DRing::FrameBuffer> ret;
+ {
+ std::lock_guard<std::mutex> guard(windows_mutex);
+ ret = std::move(windows.at(window));
+ }
+ if (not ret) {
+ __android_log_print(ANDROID_LOG_WARN, "videomanager.i", "Creating new video buffer of %zu kib", bytes/1024);
+ ret.reset(new DRing::FrameBuffer());
+ }
+ ret->storage.resize(bytes);
+ ret->ptr = ret->storage.data();
+ ret->ptrSize = bytes;
+ return ret;
+ } catch (...) {
+ return {};
+ }
+}
+
+JNIEXPORT void JNICALL Java_cx_ring_service_RingserviceJNI_registerVideoCallback(JNIEnv *jenv, jclass jcls, jstring sinkId, jlong window)
+{
+ if(!sinkId) {
+ SWIG_JavaThrowException(jenv, SWIG_JavaNullPointerException, "null string");
+ return 0;
+ }
+ const char *arg1_pstr = (const char *)jenv->GetStringUTFChars(sinkId, 0);
+ if (!arg1_pstr)
+ return 0;
+ const std::string sink(arg1_pstr);
+ jenv->ReleaseStringUTFChars(sinkId, arg1_pstr);
+
+ ANativeWindow* nativeWindow = (ANativeWindow*)((intptr_t) window);
+ auto f_display_cb = std::bind(&AndroidDisplayCb, nativeWindow, std::placeholders::_1);
+ auto p_display_cb = std::bind(&sinkTargetPullCallback, nativeWindow, std::placeholders::_1);
+
+ std::lock_guard<std::mutex> guard(windows_mutex);
+ windows.emplace(nativeWindow, nullptr);
+ DRing::registerSinkTarget(sink, DRing::SinkTarget {.pull=p_display_cb, .push=f_display_cb});
+}
+
+JNIEXPORT void JNICALL Java_cx_ring_service_RingserviceJNI_unregisterVideoCallback(JNIEnv *jenv, jclass jcls, jstring sinkId, jlong window)
+{
+ if(!sinkId) {
+ SWIG_JavaThrowException(jenv, SWIG_JavaNullPointerException, "null string");
+ return 0;
+ }
+ const char *arg1_pstr = (const char *)jenv->GetStringUTFChars(sinkId, 0);
+ if (!arg1_pstr)
+ return 0;
+ const std::string sink(arg1_pstr);
+ jenv->ReleaseStringUTFChars(sinkId, arg1_pstr);
+
+ std::lock_guard<std::mutex> guard(windows_mutex);
+ DRing::registerSinkTarget(sink, DRing::SinkTarget {});
+ windows.erase(window);
+}
+
+%}
+%native(setVideoFrame) void setVideoFrame(void *, int, long);
+%native(acquireNativeWindow) jlong acquireNativeWindow(jobject);
+%native(releaseNativeWindow) void releaseNativeWindow(jlong);
+%native(setNativeWindowGeometry) void setNativeWindowGeometry(jlong, int, int);
+%native(registerVideoCallback) void registerVideoCallback(jstring, jlong);
+%native(unregisterVideoCallback) void unregisterVideoCallback(jstring, jlong);
+
+
+namespace DRing {
+
+void setDefaultDevice(const std::string& name);
+std::string getDefaultDevice();
+
+void startCamera();
+void stopCamera();
+bool hasCameraStarted();
+bool switchInput(const std::string& resource);
+bool switchToCamera();
+
+void addVideoDevice(const std::string &node);
+void removeVideoDevice(const std::string &node);
+long obtainFrame(int length);
+void releaseFrame(long frame);
+void registerSinkTarget(const std::string& sinkId, const DRing::SinkTarget& target);
+}
+
+class VideoCallback {
+public:
+ virtual ~VideoCallback(){}
+ virtual void getCameraInfo(const std::string& device, std::vector<int> *formats, std::vector<unsigned> *sizes, std::vector<unsigned> *rates){}
+ virtual void setParameters(const std::string, const int format, const int width, const int height, const int rate) {}
+ virtual void startCapture(const std::string& camid) {}
+ virtual void stopCapture() {}
+ virtual void decodingStarted(const std::string& id, const std::string& shm_path, int w, int h, bool is_mixer) {}
+ virtual void decodingStopped(const std::string& id, const std::string& shm_path, bool is_mixer) {}
+};
diff --git a/ring-android/app/src/main/res/drawable-hdpi/ic_camera_front_white_24dp.png b/ring-android/app/src/main/res/drawable-hdpi/ic_camera_front_white_24dp.png
new file mode 100644
index 0000000..f36dcd4
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable-hdpi/ic_camera_front_white_24dp.png
Binary files differ
diff --git a/ring-android/app/src/main/res/drawable-hdpi/ic_camera_rear_white_24dp.png b/ring-android/app/src/main/res/drawable-hdpi/ic_camera_rear_white_24dp.png
new file mode 100644
index 0000000..4a5494f
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable-hdpi/ic_camera_rear_white_24dp.png
Binary files differ
diff --git a/ring-android/app/src/main/res/drawable-mdpi/ic_camera_front_white_24dp.png b/ring-android/app/src/main/res/drawable-mdpi/ic_camera_front_white_24dp.png
new file mode 100644
index 0000000..0392d64
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable-mdpi/ic_camera_front_white_24dp.png
Binary files differ
diff --git a/ring-android/app/src/main/res/drawable-mdpi/ic_camera_rear_white_24dp.png b/ring-android/app/src/main/res/drawable-mdpi/ic_camera_rear_white_24dp.png
new file mode 100644
index 0000000..e1f0f1f
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable-mdpi/ic_camera_rear_white_24dp.png
Binary files differ
diff --git a/ring-android/app/src/main/res/drawable-xhdpi/ic_camera_front_white_24dp.png b/ring-android/app/src/main/res/drawable-xhdpi/ic_camera_front_white_24dp.png
new file mode 100644
index 0000000..a80ccde
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable-xhdpi/ic_camera_front_white_24dp.png
Binary files differ
diff --git a/ring-android/app/src/main/res/drawable-xhdpi/ic_camera_rear_white_24dp.png b/ring-android/app/src/main/res/drawable-xhdpi/ic_camera_rear_white_24dp.png
new file mode 100644
index 0000000..b0768e3
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable-xhdpi/ic_camera_rear_white_24dp.png
Binary files differ
diff --git a/ring-android/app/src/main/res/drawable-xxhdpi/ic_camera_front_white_24dp.png b/ring-android/app/src/main/res/drawable-xxhdpi/ic_camera_front_white_24dp.png
new file mode 100644
index 0000000..3eb24d1
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable-xxhdpi/ic_camera_front_white_24dp.png
Binary files differ
diff --git a/ring-android/app/src/main/res/drawable-xxhdpi/ic_camera_rear_white_24dp.png b/ring-android/app/src/main/res/drawable-xxhdpi/ic_camera_rear_white_24dp.png
new file mode 100644
index 0000000..8392b2a
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable-xxhdpi/ic_camera_rear_white_24dp.png
Binary files differ
diff --git a/ring-android/app/src/main/res/drawable-xxxhdpi/ic_camera_front_white_24dp.png b/ring-android/app/src/main/res/drawable-xxxhdpi/ic_camera_front_white_24dp.png
new file mode 100644
index 0000000..951bc12
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable-xxxhdpi/ic_camera_front_white_24dp.png
Binary files differ
diff --git a/ring-android/app/src/main/res/drawable-xxxhdpi/ic_camera_rear_white_24dp.png b/ring-android/app/src/main/res/drawable-xxxhdpi/ic_camera_rear_white_24dp.png
new file mode 100644
index 0000000..8b2415b
--- /dev/null
+++ b/ring-android/app/src/main/res/drawable-xxxhdpi/ic_camera_rear_white_24dp.png
Binary files differ
diff --git a/ring-android/app/src/main/res/layout/activity_call_layout.xml b/ring-android/app/src/main/res/layout/activity_call_layout.xml
index 95a192e..e8f973d 100644
--- a/ring-android/app/src/main/res/layout/activity_call_layout.xml
+++ b/ring-android/app/src/main/res/layout/activity_call_layout.xml
@@ -19,11 +19,12 @@
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<cx.ring.views.FitsWindowsLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/slidingpanelayout"
+ android:id="@+id/maincalllayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:background="@color/cardview_dark_background"
tools:context=".client.CallActivity">
<!--
@@ -37,12 +38,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="right|center_vertical" />
--->
+
<fragment
android:id="@+id/ongoingcall_pane"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="cx.ring.fragments.CallFragment"
- tools:layout="@layout/frag_call" />
+ tools:layout="@layout/frag_call" />-->
-</FrameLayout>
\ No newline at end of file
+</cx.ring.views.FitsWindowsLayout>
\ No newline at end of file
diff --git a/ring-android/app/src/main/res/layout/frag_call.xml b/ring-android/app/src/main/res/layout/frag_call.xml
index d76d6ea..1ea59b9 100644
--- a/ring-android/app/src/main/res/layout/frag_call.xml
+++ b/ring-android/app/src/main/res/layout/frag_call.xml
@@ -1,161 +1,200 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<?xml version="1.0" encoding="utf-8"?><!--
+Copyright (C) 2004-2016 Savoir-faire Linux Inc.
+
+Author: Adrien Beraud <adrien.beraud@savoirfairelinux.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".client.CallActivity">
- <RelativeLayout
- android:id="@+id/contact_bubble_layout"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:layout_centerHorizontal="true"
- android:layout_centerVertical="true"
- android:layout_marginBottom="16dp">
+ <SurfaceView
+ android:id="@+id/video_preview_surface"
+ android:layout_width="match_parent"
+ android:layout_height="32dp"
+ android:layout_alignParentEnd="false"
+ android:layout_alignParentStart="false"
+ android:layout_centerInParent="true"
+ android:layout_gravity="center"
+ android:visibility="gone" />
- <ImageView
- android:id="@+id/contact_bubble"
- android:layout_width="200dp"
- android:layout_height="200dp"
+ <FrameLayout
+ android:id="@+id/inner_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <SurfaceView
+ android:id="@+id/camera_preview_surface"
+ android:layout_width="160dp"
+ android:layout_height="120dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_margin="8dp"
+ android:layout_gravity="bottom|right"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:id="@+id/contact_bubble_layout"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
- android:layout_marginLeft="16dp"
- android:layout_marginRight="16dp"
- android:layout_alignParentStart="false"
- android:layout_alignParentTop="true"
- android:layout_marginBottom="16dp" />
-
- <TextView
- android:id="@+id/contact_bubble_txt"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_marginBottom="16dp"
+ android:orientation="vertical"
android:gravity="center_horizontal"
- android:textAppearance="?android:attr/textAppearanceLarge"
- android:textColor="@color/text_color_primary"
- android:text="Adrien Béraud"
- android:layout_below="@+id/contact_bubble" />
+ android:layout_gravity="center_vertical">
- <TextView
- android:id="@+id/contact_bubble_num_txt"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:gravity="center_horizontal"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:textColor="@color/text_color_secondary"
- android:text="(514) 279-2035"
- android:layout_alignParentEnd="false"
- android:layout_below="@+id/contact_bubble_txt" />
+ <ImageView
+ android:id="@+id/contact_bubble"
+ android:layout_width="160dp"
+ android:layout_height="160dp"
+ android:layout_alignParentStart="false"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:layout_marginBottom="16dp"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp" />
- </RelativeLayout>
+ <TextView
+ android:id="@+id/contact_bubble_txt"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/contact_bubble"
+ android:gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="@color/text_color_primary_dark" />
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:layout_alignParentBottom="true"
- android:layout_centerHorizontal="true"
- android:layout_margin="24dp">
+ <TextView
+ android:id="@+id/contact_bubble_num_txt"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="false"
+ android:layout_below="@+id/contact_bubble_txt"
+ android:gravity="center_horizontal"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="@color/text_color_secondary_dark"
+ android:singleLine="true"
+ android:ellipsize="middle"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp" />
- <android.support.design.widget.FloatingActionButton
- android:id="@+id/call_refuse_btn"
+ <TextView
+ android:id="@+id/call_status_txt"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:textColor="@color/text_color_primary_dark"
+ android:textSize="16sp"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ </LinearLayout>
+
+ <LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_margin="16dp"
- android:contentDescription="@string/action_call_decline"
+ android:orientation="horizontal"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:layout_margin="12dp"
+ android:layout_gravity="bottom|center_horizontal">
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/call_refuse_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:contentDescription="@string/action_call_decline"
+ android:src="@drawable/ic_call_end_white_24dp"
+ app:backgroundTint="@color/error_red"
+ app:elevation="6dp"
+ app:pressedTranslationZ="12dp"
+ app:rippleColor="@android:color/white" />
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/call_accept_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:contentDescription="@string/action_call_accept"
+ android:src="@drawable/ic_call_white_24dp"
+ app:backgroundTint="#4caf50"
+ app:elevation="6dp"
+ app:pressedTranslationZ="12dp"
+ app:rippleColor="@android:color/white" />
+ </LinearLayout>
+
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/call_hangup_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:layout_margin="28dp"
android:src="@drawable/ic_call_end_white_24dp"
app:backgroundTint="@color/error_red"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
- app:rippleColor="@android:color/white" />
+ app:rippleColor="@android:color/white"
+ android:layout_gravity="bottom|center_horizontal" />
- <android.support.design.widget.FloatingActionButton
- android:id="@+id/call_accept_btn"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_margin="16dp"
- android:contentDescription="@string/action_call_accept"
- android:src="@drawable/ic_call_white_24dp"
- app:backgroundTint="#4caf50"
- app:elevation="6dp"
- app:pressedTranslationZ="12dp"
- app:rippleColor="@android:color/white" />
- </LinearLayout>
+ <RelativeLayout
+ android:id="@+id/call_status_bar"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/actionBarSize"
+ android:layout_alignParentTop="true"
+ android:visibility="visible">
- <android.support.design.widget.FloatingActionButton
- android:id="@+id/call_hangup_btn"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerHorizontal="true"
- android:layout_margin="40dp"
- android:src="@drawable/ic_call_end_white_24dp"
- app:backgroundTint="@color/error_red"
- app:elevation="6dp"
- app:pressedTranslationZ="12dp"
- app:rippleColor="@android:color/white"
- android:layout_alignParentBottom="true" />
-
- <RelativeLayout
- android:id="@+id/call_status_bar"
- android:layout_width="match_parent"
- android:layout_height="?android:attr/actionBarSize"
- android:layout_alignParentTop="true"
- android:visibility="visible">
-
- <ImageView
- android:id="@+id/image_call"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerVertical="true"
- android:layout_gravity="left"
- android:layout_marginLeft="15dp"
- android:layout_marginRight="10dp"
- android:src="@drawable/ic_call_white_24dp"
- android:tint="@android:color/black" />
-
- <TextView
- android:id="@+id/call_status_txt"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerVertical="true"
- android:layout_toRightOf="@+id/image_call"
- android:textColor="@color/text_color_primary"
- android:textSize="12sp" />
-
- <ViewSwitcher
- android:id="@+id/security_switcher"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerVertical="true"
- android:visibility="gone">
-
- <Button
- android:id="@+id/confirm_sas"
+ <ViewSwitcher
+ android:id="@+id/security_switcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:textColor="@color/white"
- android:textSize="12sp" />
+ android:layout_centerVertical="true"
+ android:visibility="gone">
+
+ <Button
+ android:id="@+id/confirm_sas"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:textColor="@android:color/white"
+ android:textSize="12sp" />
+
+ <ImageView
+ android:id="@+id/lock_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|center_vertical" />
+ </ViewSwitcher>
<ImageView
- android:id="@+id/lock_image"
+ android:id="@+id/security_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="end|center_vertical" />
- </ViewSwitcher>
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:layout_margin="16dp"
+ android:src="@drawable/ic_lock_white_24dp"
+ android:tint="#4caf50"
+ android:visibility="gone" />
-
- <ImageView
- android:id="@+id/security_indicator"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentEnd="true"
- android:layout_alignParentRight="true"
- android:layout_centerVertical="true"
- android:layout_margin="16dp"
- android:src="@drawable/ic_lock_white_24dp"
- android:tint="#4caf50"
- android:visibility="gone" />
-
- </RelativeLayout>
-
-</RelativeLayout>
\ No newline at end of file
+ </RelativeLayout>
+ </FrameLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/ring-android/app/src/main/res/menu/ac_call.xml b/ring-android/app/src/main/res/menu/ac_call.xml
index 04f7155..6f78d8e 100644
--- a/ring-android/app/src/main/res/menu/ac_call.xml
+++ b/ring-android/app/src/main/res/menu/ac_call.xml
@@ -17,6 +17,12 @@
android:checkable="true" />
<item
+ android:id="@+id/menuitem_camera_flip"
+ app:showAsAction="always"
+ android:icon="@drawable/ic_camera_front_white_24dp"
+ android:title="@string/ab_action_flipcamera" />
+
+ <item
android:id="@+id/menuitem_addcontact"
app:showAsAction="always"
android:icon="@drawable/ic_person_add_white_24dp"
diff --git a/ring-android/app/src/main/res/values-v21/styles.xml b/ring-android/app/src/main/res/values-v21/styles.xml
index 89b7d60..7f22be9 100644
--- a/ring-android/app/src/main/res/values-v21/styles.xml
+++ b/ring-android/app/src/main/res/values-v21/styles.xml
@@ -14,12 +14,31 @@
</style>
+ <style name="AppTheme.ActionBar.Transparent" parent="style/Theme.AppCompat">
+ <item name="colorAccent">@color/color_primary_dark</item>
+ <item name="colorPrimary">@color/color_primary_light</item>
+ <item name="colorPrimaryDark">@color/color_primary_dark</item>
+ <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
+ <item name="android:actionBarStyle">@style/ActionBar.Transparent</item>
+ <item name="actionBarStyle">@style/ActionBar.Transparent</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="windowActionBarOverlay">true</item>
+ <item name="android:windowTranslucentStatus">true</item>
+ <item name="android:windowTranslucentNavigation">true</item>
+ </style>
+
<style name="NativeActionBar" parent="@android:style/Widget.ActionBar">
<item name="android:background">@color/actionbar</item>
<item name="android:titleTextStyle">@style/NativeActionBar.Text</item>
<item name="android:elevation">4dp</item>
</style>
+ <style name="ActionBar.Transparent" parent="@android:style/Widget.DeviceDefault.Light.ActionBar.Solid.Inverse">
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:height">@dimen/abc_action_bar_default_height_material</item>
+ <item name="background">@android:color/transparent</item>
+ </style>
+
<style name="NativeActionBar.Text" parent="@android:style/TextAppearance.Material.Widget.ActionBar.Title">
</style>
</resources>
\ No newline at end of file
diff --git a/ring-android/app/src/main/res/values/strings.xml b/ring-android/app/src/main/res/values/strings.xml
index 6782b19..bff090b 100644
--- a/ring-android/app/src/main/res/values/strings.xml
+++ b/ring-android/app/src/main/res/values/strings.xml
@@ -131,5 +131,6 @@
<string name="share_via">Share via</string>
<string name="write_a_message">Write a message</string>
<string name="scan_qr">Scan QR Code</string>
+ <string name="ab_action_flipcamera">Flip camera</string>
</resources>
diff --git a/ring-android/app/src/main/res/values/styles.xml b/ring-android/app/src/main/res/values/styles.xml
index 4d7f03a..41c4471 100644
--- a/ring-android/app/src/main/res/values/styles.xml
+++ b/ring-android/app/src/main/res/values/styles.xml
@@ -35,6 +35,19 @@
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
</style>
+ <style name="AppTheme.ActionBar.Transparent" parent="style/Theme.AppCompat">
+ <item name="colorAccent">@color/color_primary_dark</item>
+ <item name="colorPrimary">@color/color_primary_light</item>
+ <item name="colorPrimaryDark">@color/color_primary_dark</item>
+ <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
+ <item name="android:actionBarStyle">@style/ActionBar.Transparent</item>
+ <item name="actionBarStyle">@style/ActionBar.Transparent</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="windowActionBarOverlay">true</item>
+ <item name="android:windowTranslucentStatus">true</item>
+ <item name="android:windowTranslucentNavigation">true</item>
+ </style>
+
<style name="AppThemeWithoutOverlayCompatNoShadow" parent="AppThemeWithoutOverlayCompat">
<item name="android:windowContentOverlay">@null</item>
</style>
@@ -45,6 +58,12 @@
<item name="elevation">4dp</item>
</style>
+ <style name="ActionBar.Transparent" parent="@android:style/Widget.DeviceDefault.Light.ActionBar.Solid.Inverse">
+ <item name="android:background">@null</item>
+ <item name="background">@null</item>
+ <item name="android:height">@dimen/abc_action_bar_default_height_material</item>
+ </style>
+
<style name="MyActionBar" parent="@style/Widget.AppCompat.ActionBar.Solid">
<item name="android:height">@dimen/abc_action_bar_default_height_material</item>
<item name="android:textColorPrimary">@color/text_color_primary_dark</item>