android: SDK v2 pass one

Add JitsiMeetFragment and refactor the app to use it.
This commit is contained in:
Saúl Ibarra Corretgé 2019-01-24 16:50:15 +01:00 committed by Saúl Ibarra Corretgé
parent dbc88b972e
commit 90803c8ff6
7 changed files with 187 additions and 218 deletions

View File

@ -17,105 +17,121 @@
package org.jitsi.meet; package org.jitsi.meet;
import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.provider.Settings;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
import android.view.KeyEvent;
import org.jitsi.meet.sdk.JitsiMeetActivity; import org.jitsi.meet.sdk.JitsiMeet;
import org.jitsi.meet.sdk.JitsiMeetView; import org.jitsi.meet.sdk.JitsiMeetFragment;
import org.jitsi.meet.sdk.JitsiMeetViewListener; import org.jitsi.meet.sdk.JitsiMeetActivityInterface;
import org.jitsi.meet.sdk.JitsiMeetActivityDelegate;
import com.crashlytics.android.Crashlytics; import com.crashlytics.android.Crashlytics;
import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.modules.core.PermissionListener;
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks; import com.google.firebase.dynamiclinks.FirebaseDynamicLinks;
import io.fabric.sdk.android.Fabric; import io.fabric.sdk.android.Fabric;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
/** /**
* The one and only {@link Activity} that the Jitsi Meet app needs. The * The one and only {@link Activity} that the Jitsi Meet app needs. The
* {@code Activity} is launched in {@code singleTask} mode, so it will be * {@code Activity} is launched in {@code singleTask} mode, so it will be
* created upon application initialization and there will be a single instance * created upon application initialization and there will be a single instance
* of it. Further attempts at launching the application once it was already * of it. Further attempts at launching the application once it was already
* launched will result in {@link Activity#onNewIntent(Intent)} being called. * launched will result in {@link Activity#onNewIntent(Intent)} being called.
*
* This {@code Activity} extends {@link JitsiMeetActivity} to keep the React
* Native CLI working, since the latter always tries to launch an
* {@code Activity} named {@code MainActivity} when doing
* {@code react-native run-android}.
*/ */
public class MainActivity extends JitsiMeetActivity { public class MainActivity extends FragmentActivity implements JitsiMeetActivityInterface {
/**
* The request code identifying requests for the permission to draw on top
* of other apps. The value must be 16-bit and is arbitrarily chosen here.
*/
private static final int OVERLAY_PERMISSION_REQUEST_CODE
= (int) (Math.random() * Short.MAX_VALUE);
@Override private JitsiMeetFragment getFragment() {
protected JitsiMeetView initializeView() { return (JitsiMeetFragment) getSupportFragmentManager().findFragmentById(R.id.jitsiFragment);
JitsiMeetView view = super.initializeView(); }
// XXX In order to increase (1) awareness of API breakages and (2) API private void initialize() {
// coverage, utilize JitsiMeetViewListener in the Debug configuration of JitsiMeetFragment fragment = getFragment();
// the app. fragment.setWelcomePageEnabled(true);
if (BuildConfig.DEBUG && view != null) { fragment.loadURL(getIntentUrl(getIntent()));
view.setListener(new JitsiMeetViewListener() { }
private void on(String name, Map<String, Object> data) {
UiThreadUtil.assertOnUiThread();
// Log with the tag "ReactNative" in order to have the log private @Nullable String getIntentUrl(Intent intent) {
// visible in react-native log-android as well. Uri uri;
Log.d(
"ReactNative",
JitsiMeetViewListener.class.getSimpleName() + " "
+ name + " "
+ data);
}
@Override
public void onConferenceFailed(Map<String, Object> data) {
on("CONFERENCE_FAILED", data);
}
@Override
public void onConferenceJoined(Map<String, Object> data) {
on("CONFERENCE_JOINED", data);
}
@Override
public void onConferenceLeft(Map<String, Object> data) {
on("CONFERENCE_LEFT", data);
}
@Override
public void onConferenceWillJoin(Map<String, Object> data) {
on("CONFERENCE_WILL_JOIN", data);
}
@Override
public void onConferenceWillLeave(Map<String, Object> data) {
on("CONFERENCE_WILL_LEAVE", data);
}
@Override
public void onLoadConfigError(Map<String, Object> data) {
on("LOAD_CONFIG_ERROR", data);
}
});
if (Intent.ACTION_VIEW.equals(intent.getAction())
&& (uri = intent.getData()) != null) {
return uri.toString();
} }
return view; return null;
}
private boolean canRequestOverlayPermission() {
return
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.M;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE
&& canRequestOverlayPermission()) {
if (Settings.canDrawOverlays(this)) {
initialize();
}
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onBackPressed() {
JitsiMeetActivityDelegate.onBackPressed();
}
// ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_MENU) {
JitsiMeet.showDevOptions();
return true;
}
return super.onKeyUp(keyCode, event);
}
@Override
public void onNewIntent(Intent intent) {
String url;
if ((url = getIntentUrl(intent)) != null) {
getFragment().loadURL(url);
return;
}
JitsiMeetActivityDelegate.onNewIntent(intent);
}
@Override
protected void onUserLeaveHint() {
getFragment().enterPictureInPicture();
} }
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
// As this is the Jitsi Meet app (i.e. not the Jitsi Meet SDK), we do
// want to enable some options.
// The welcome page defaults to disabled in the SDK at the time of this
// writing but it is clearer to be explicit about what we want anyway.
setWelcomePageEnabled(true);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Set the Activity's content view.
setContentView(R.layout.main_layout);
// Setup Crashlytics and Firebase Dynamic Links // Setup Crashlytics and Firebase Dynamic Links
if (BuildConfig.GOOGLE_SERVICES_ENABLED) { if (BuildConfig.GOOGLE_SERVICES_ENABLED) {
Fabric.with(this, new Crashlytics()); Fabric.with(this, new Crashlytics());
@ -129,14 +145,31 @@ public class MainActivity extends JitsiMeetActivity {
} }
if (dynamicLink != null) { if (dynamicLink != null) {
try { getFragment().loadURL(dynamicLink.toString());
loadURL(new URL(dynamicLink.toString()));
} catch (MalformedURLException e) {
Log.d("ReactNative", "Malformed dynamic link", e);
}
} }
}); });
} }
// In Debug builds React needs permission to write over other apps in
// order to display the warning and error overlays.
if (BuildConfig.DEBUG) {
if (canRequestOverlayPermission() && !Settings.canDrawOverlays(this)) {
Intent intent
= new Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE);
return;
}
}
initialize();
}
@Override
public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener) {
JitsiMeetActivityDelegate.requestPermissions(this, permissions, requestCode, listener);
} }
} }

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="org.jitsi.meet.sdk.JitsiMeetFragment"
android:id="@+id/jitsiFragment"/>
</FrameLayout>

View File

@ -22,7 +22,7 @@ import android.app.Activity;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
/** /**
* Defines the default behavior of {@code JitsiMeetActivity} and * Defines the default behavior of {@code JitsiMeetFragment} and
* {@code JitsiMeetView} upon invoking the back button if no * {@code JitsiMeetView} upon invoking the back button if no
* {@code JitsiMeetView} handles the invocation. For example, a * {@code JitsiMeetView} handles the invocation. For example, a
* {@code JitsiMeetView} may (1) handle the invocation of the back button * {@code JitsiMeetView} may (1) handle the invocation of the back button

View File

@ -0,0 +1,30 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import com.facebook.react.ReactInstanceManager;
public class JitsiMeet {
public static void showDevOptions() {
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager != null) {
reactInstanceManager.showDevOptionsDialog();
}
}
}

View File

@ -32,7 +32,13 @@ import com.facebook.react.modules.core.PermissionListener;
* {@link Activity} lifecycle methods in order for the React side to be aware of * {@link Activity} lifecycle methods in order for the React side to be aware of
* it. * it.
*/ */
public class ReactActivityLifecycleCallbacks { public class JitsiMeetActivityDelegate {
/**
* Needed for making sure this class working with the "PermissionsAndroid"
* React Native module.
*/
private static PermissionListener permissionListener;
private static Callback permissionsCallback;
/** /**
* {@link Activity} lifecycle method which should be called from * {@link Activity} lifecycle method which should be called from
@ -57,13 +63,6 @@ public class ReactActivityLifecycleCallbacks {
} }
} }
/**
* Needed for making sure this class working with the "PermissionsAndroid"
* React Native module.
*/
private static PermissionListener permissionListener;
private static Callback permissionsCallback;
/** /**
* {@link Activity} lifecycle method which should be called from * {@link Activity} lifecycle method which should be called from
* {@link Activity#onBackPressed} so we can do the required internal * {@link Activity#onBackPressed} so we can do the required internal

View File

@ -6,7 +6,7 @@ import com.facebook.react.modules.core.PermissionAwareActivity;
/** /**
* This interface serves as the umbrella interface that applications not using * This interface serves as the umbrella interface that applications not using
* {@code JitsiMeetActivity} must implement in order to ensure full * {@code JitsiMeetFragment} must implement in order to ensure full
* functionality. * functionality.
*/ */
public interface JitsiMeetActivityInterface public interface JitsiMeetActivityInterface

View File

@ -18,16 +18,13 @@
package org.jitsi.meet.sdk; package org.jitsi.meet.sdk;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Settings; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity; import android.support.v4.app.Fragment;
import android.view.KeyEvent; import android.view.LayoutInflater;
import android.view.View;
import com.facebook.react.ReactInstanceManager; import android.view.ViewGroup;
import com.facebook.react.modules.core.PermissionListener;
import java.net.URL; import java.net.URL;
@ -42,15 +39,7 @@ import java.net.URL;
* hooked to the React Native subsystem via proxy calls through the * hooked to the React Native subsystem via proxy calls through the
* {@code JitsiMeetView} static methods. * {@code JitsiMeetView} static methods.
*/ */
public class JitsiMeetActivity public class JitsiMeetFragment extends Fragment {
extends AppCompatActivity implements JitsiMeetActivityInterface {
/**
* The request code identifying requests for the permission to draw on top
* of other apps. The value must be 16-bit and is arbitrarily chosen here.
*/
private static final int OVERLAY_PERMISSION_REQUEST_CODE
= (int) (Math.random() * Short.MAX_VALUE);
/** /**
* A color scheme object to override the default color is the SDK. * A color scheme object to override the default color is the SDK.
@ -81,14 +70,6 @@ public class JitsiMeetActivity
*/ */
private boolean welcomePageEnabled; private boolean welcomePageEnabled;
private boolean canRequestOverlayPermission() {
return
BuildConfig.DEBUG
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& getApplicationInfo().targetSdkVersion
>= Build.VERSION_CODES.M;
}
/** /**
* *
* @see JitsiMeetView#getDefaultURL() * @see JitsiMeetView#getDefaultURL()
@ -97,22 +78,12 @@ public class JitsiMeetActivity
return view == null ? defaultURL : view.getDefaultURL(); return view == null ? defaultURL : view.getDefaultURL();
} }
/** @Nullable
* Initializes the {@link #view} of this {@code JitsiMeetActivity} with a @Override
* new {@link JitsiMeetView} instance. public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
*/ this.view = initializeView();
private void initializeContentView() {
JitsiMeetView view = initializeView();
if (view != null) { return this.view;
// XXX Allow extenders who override initializeView() to configure
// the view before the first loadURL(). Probably works around a
// problem related to ReactRootView#setAppProperties().
view.loadURL(null);
this.view = view;
setContentView(this.view);
}
} }
/** /**
@ -121,7 +92,7 @@ public class JitsiMeetActivity
* @return a new {@code JitsiMeetView} instance. * @return a new {@code JitsiMeetView} instance.
*/ */
protected JitsiMeetView initializeView() { protected JitsiMeetView initializeView() {
JitsiMeetView view = new JitsiMeetView(this); JitsiMeetView view = new JitsiMeetView(getActivity());
// XXX Before calling JitsiMeetView#loadURL, make sure to call whatever // XXX Before calling JitsiMeetView#loadURL, make sure to call whatever
// is documented to need such an order in order to take effect: // is documented to need such an order in order to take effect:
@ -161,97 +132,31 @@ public class JitsiMeetActivity
* *
* @param url The conference URL. * @param url The conference URL.
*/ */
public void loadURL(@Nullable URL url) { public void loadURL(@Nullable String url) {
view.loadURL(url); view.loadURLString(url);
} }
@Override @Override
protected void onActivityResult( public void onActivityResult(int requestCode, int resultCode, Intent data) {
int requestCode, JitsiMeetActivityDelegate.onActivityResult(
int resultCode, getActivity(), requestCode, resultCode, data);
Intent data) {
if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE
&& canRequestOverlayPermission()) {
if (Settings.canDrawOverlays(this)) {
initializeContentView();
}
return;
}
ReactActivityLifecycleCallbacks.onActivityResult(
this, requestCode, resultCode, data);
} }
@Override @Override
public void onBackPressed() { public void onDestroyView() {
ReactActivityLifecycleCallbacks.onBackPressed();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// In Debug builds React needs permission to write over other apps in
// order to display the warning and error overlays.
if (canRequestOverlayPermission() && !Settings.canDrawOverlays(this)) {
Intent intent
= new Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE);
return;
}
initializeContentView();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (view != null) { if (view != null) {
view.dispose(); view.dispose();
view = null; view = null;
} }
ReactActivityLifecycleCallbacks.onHostDestroy(this); super.onDestroyView();
}
// ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
ReactInstanceManager reactInstanceManager;
if (!super.onKeyUp(keyCode, event)
&& BuildConfig.DEBUG
&& (reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager())
!= null
&& keyCode == KeyEvent.KEYCODE_MENU) {
reactInstanceManager.showDevOptionsDialog();
return true;
}
return false;
} }
@Override @Override
public void onNewIntent(Intent intent) { public void onDestroy() {
// XXX At least twice we received bug reports about malfunctioning super.onDestroy();
// loadURL in the Jitsi Meet SDK while the Jitsi Meet app seemed to
// functioning as expected in our testing. But that was to be expected
// because the app does not exercise loadURL. In order to increase the
// test coverage of loadURL, channel deep linking through loadURL.
Uri uri;
if (Intent.ACTION_VIEW.equals(intent.getAction()) JitsiMeetActivityDelegate.onHostDestroy(getActivity());
&& (uri = intent.getData()) != null
&& JitsiMeetView.loadURLStringInViews(uri.toString())) {
return;
}
ReactActivityLifecycleCallbacks.onNewIntent(intent);
} }
// https://developer.android.com/reference/android/support/v4/app/ActivityCompat.OnRequestPermissionsResultCallback // https://developer.android.com/reference/android/support/v4/app/ActivityCompat.OnRequestPermissionsResultCallback
@ -260,39 +165,31 @@ public class JitsiMeetActivity
final int requestCode, final int requestCode,
final String[] permissions, final String[] permissions,
final int[] grantResults) { final int[] grantResults) {
ReactActivityLifecycleCallbacks.onRequestPermissionsResult(requestCode, permissions, grantResults); JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
} }
@Override @Override
protected void onResume() { public void onResume() {
super.onResume(); super.onResume();
ReactActivityLifecycleCallbacks.onHostResume(this); JitsiMeetActivityDelegate.onHostResume(getActivity());
} }
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
ReactActivityLifecycleCallbacks.onHostPause(this); JitsiMeetActivityDelegate.onHostPause(getActivity());
} }
@Override public void enterPictureInPicture() {
protected void onUserLeaveHint() {
if (view != null) { if (view != null) {
view.enterPictureInPicture(); view.enterPictureInPicture();
} }
} }
/** /**
* Implementation of the {@code PermissionAwareActivity} interface. *
*/
@Override
public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener) {
ReactActivityLifecycleCallbacks.requestPermissions(this, permissions, requestCode, listener);
}
/**
* @see JitsiMeetView#setColorScheme(Bundle) * @see JitsiMeetView#setColorScheme(Bundle)
*/ */
public void setColorScheme(Bundle colorScheme) { public void setColorScheme(Bundle colorScheme) {