blob: ecc84c8d730b915ffb9ce21322845e5dbb024ab8 [file] [log] [blame]
// Copyright 2017 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dev.cobalt.coat;
import static dev.cobalt.util.Log.TAG;
import android.app.NativeActivity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import dev.cobalt.media.VideoSurfaceView;
import dev.cobalt.util.Log;
import dev.cobalt.util.UsedByNative;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** Native activity that has the required JNI methods called by the Starboard implementation. */
public abstract class CobaltActivity extends NativeActivity {
// A place to put args while debugging so they're used even when starting from the launcher.
// This should always be empty in submitted code.
private static final String[] DEBUG_ARGS = {};
private static final String URL_ARG = "--url=";
private static final java.lang.String META_DATA_APP_URL = "cobalt.APP_URL";
private static final String SPLASH_URL_ARG = "--fallback_splash_screen_url=";
private static final java.lang.String META_DATA_SPLASH_URL = "cobalt.SPLASH_URL";
private static final String FORCE_MIGRATION_FOR_STORAGE_PARTITIONING =
"--force_migration_for_storage_partitioning";
private static final String META_FORCE_MIGRATION_FOR_STORAGE_PARTITIONING =
"cobalt.force_migration_for_storage_partitioning";
@SuppressWarnings("unused")
private CobaltA11yHelper a11yHelper;
private VideoSurfaceView videoSurfaceView;
private KeyboardEditor keyboardEditor;
private ViewTreeObserver.OnGlobalLayoutListener videoSurfaceLayoutListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
// To ensure that volume controls adjust the correct stream, make this call
// early in the app's lifecycle. This connects the volume controls to
// STREAM_MUSIC whenever the target activity or fragment is visible.
setVolumeControlStream(AudioManager.STREAM_MUSIC);
String startDeepLink = getIntentUrlAsString(getIntent());
if (getStarboardBridge() == null) {
// Cold start - Instantiate the singleton StarboardBridge.
StarboardBridge starboardBridge = createStarboardBridge(getArgs(), startDeepLink);
((StarboardBridge.HostApplication) getApplication()).setStarboardBridge(starboardBridge);
} else {
// Warm start - Pass the deep link to the running Starboard app.
getStarboardBridge().handleDeepLink(startDeepLink);
}
// super.onCreate() will cause an APP_CMD_START in native code,
// so make sure to initialize any state beforehand that might be touched by
// native code invocations.
super.onCreate(savedInstanceState);
videoSurfaceView = new VideoSurfaceView(this);
a11yHelper = new CobaltA11yHelper(videoSurfaceView);
addContentView(
videoSurfaceView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
videoSurfaceLayoutListener =
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
VideoSurfaceView.nativeOnGlobalLayout();
}
};
ViewTreeObserver observer = getWindow().getDecorView().getViewTreeObserver();
observer.addOnGlobalLayoutListener(videoSurfaceLayoutListener);
keyboardEditor = new KeyboardEditor(this);
addContentView(
keyboardEditor, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
}
/**
* Instantiates the StarboardBridge. Apps not supporting sign-in should inject an instance of
* NoopUserAuthorizer. Apps may subclass StarboardBridge if they need to override anything.
*/
protected abstract StarboardBridge createStarboardBridge(String[] args, String startDeepLink);
@UsedByNative
protected StarboardBridge getStarboardBridge() {
return ((StarboardBridge.HostApplication) getApplication()).getStarboardBridge();
}
@Override
protected void onStart() {
getStarboardBridge().onActivityStart(this, keyboardEditor);
super.onStart();
}
@Override
protected void onStop() {
getStarboardBridge().onActivityStop(this);
super.onStop();
}
@Override
protected void onDestroy() {
ViewTreeObserver observer = getWindow().getDecorView().getViewTreeObserver();
observer.removeOnGlobalLayoutListener(videoSurfaceLayoutListener);
super.onDestroy();
getStarboardBridge().onActivityDestroy(this);
}
@Override
public boolean onSearchRequested() {
return getStarboardBridge().onSearchRequested();
}
/** Returns true if the argument list contains an arg starting with argName. */
private static boolean hasArg(List<String> args, String argName) {
for (String arg : args) {
if (arg.startsWith(argName)) {
return true;
}
}
return false;
}
/**
* Get argv/argc style args, if any from intent extras. Returns empty array if there are none
*
* <p>To use, invoke application via, eg, adb shell am start --esa args arg1,arg2 \
* dev.cobalt.coat/dev.cobalt.app.MainActivity
*/
protected String[] getArgs() {
Bundle extras = getIntent().getExtras();
CharSequence[] argsExtra =
(extras == null || isReleaseBuild()) ? null : extras.getCharSequenceArray("args");
List<String> args = new ArrayList<>(Arrays.asList(DEBUG_ARGS));
if (argsExtra != null) {
for (int i = 0; i < argsExtra.length; i++) {
args.add(argsExtra[i].toString());
}
}
// If the URL arg isn't specified, get it from AndroidManifest.xml.
boolean hasUrlArg = hasArg(args, URL_ARG);
// If the splash screen url arg isn't specified, get it from AndroidManifest.xml.
boolean hasSplashUrlArg = hasArg(args, SPLASH_URL_ARG);
if (!hasUrlArg || !hasSplashUrlArg) {
try {
ActivityInfo ai =
getPackageManager()
.getActivityInfo(getIntent().getComponent(), PackageManager.GET_META_DATA);
if (ai.metaData != null) {
if (!hasUrlArg) {
String url = ai.metaData.getString(META_DATA_APP_URL);
if (url != null) {
args.add(URL_ARG + url);
}
}
if (!hasSplashUrlArg) {
String splashUrl = ai.metaData.getString(META_DATA_SPLASH_URL);
if (splashUrl != null) {
args.add(SPLASH_URL_ARG + splashUrl);
}
}
if (ai.metaData.getBoolean(META_FORCE_MIGRATION_FOR_STORAGE_PARTITIONING)) {
args.add(FORCE_MIGRATION_FOR_STORAGE_PARTITIONING);
}
}
} catch (NameNotFoundException e) {
throw new RuntimeException("Error getting activity info", e);
}
}
return args.toArray(new String[0]);
}
protected boolean isReleaseBuild() {
return StarboardBridge.isReleaseBuild();
}
@Override
protected void onNewIntent(Intent intent) {
getStarboardBridge().handleDeepLink(getIntentUrlAsString(intent));
}
/**
* Returns the URL from an Intent as a string. This may be overridden for additional processing.
*/
protected String getIntentUrlAsString(Intent intent) {
Uri intentUri = intent.getData();
return (intentUri == null) ? null : intentUri.toString();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
getStarboardBridge().onActivityResult(requestCode, resultCode, data);
}
@Override
public void onRequestPermissionsResult(
int requestCode, String[] permissions, int[] grantResults) {
getStarboardBridge().onRequestPermissionsResult(requestCode, permissions, grantResults);
}
public void setVideoSurfaceBounds(final int x, final int y, final int width, final int height) {
if (!videoSurfaceView.updateVideoBounds(x, y, width, height)) {
return;
}
VideoSurfaceView.nativeOnLayoutNeeded();
runOnUiThread(
new Runnable() {
@Override
public void run() {
VideoSurfaceView.nativeOnLayoutScheduled();
LayoutParams layoutParams = videoSurfaceView.getLayoutParams();
// Since videoSurfaceView is added directly to the Activity's content view, which is a
// FrameLayout, we expect its layout params to become FrameLayout.LayoutParams.
if (layoutParams instanceof FrameLayout.LayoutParams) {
((FrameLayout.LayoutParams) layoutParams).setMargins(x, y, x + width, y + height);
} else {
Log.w(
TAG,
"Unexpected video surface layout params class "
+ layoutParams.getClass().getName());
}
layoutParams.width = width;
layoutParams.height = height;
// Even though as a NativeActivity we're not using the Android UI framework, by setting
// the layout params it will force a layout to be requested. That will cause the
// SurfaceView to position its underlying Surface to match the screen coordinates of
// where the view would be in a UI layout and to set the surface transform matrix to
// match the view's size.
videoSurfaceView.setLayoutParams(layoutParams);
}
});
}
}