diff --git a/app/build.gradle b/app/build.gradle index 0feb89a71..73317fd5c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -94,7 +94,7 @@ android { } lintOptions { - disable "LogUsage", "ExtraTranslation" + disable "ExtraTranslation" } packagingOptions { @@ -428,6 +428,7 @@ dependencies { implementation deps.support.vector_drawable implementation deps.support.annotations implementation deps.constraint_layout + implementation deps.gson // Android Components implementation deps.mozilla_speech diff --git a/app/src/common/shared/org/mozilla/vrbrowser/DataRepository.java b/app/src/common/shared/org/mozilla/vrbrowser/DataRepository.java index e6e9f4b82..5888a5143 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/DataRepository.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/DataRepository.java @@ -1,14 +1,5 @@ package org.mozilla.vrbrowser; -import android.content.Context; - -import org.mozilla.vrbrowser.browser.SessionStore; -import org.mozilla.vrbrowser.db.AppDatabase; -import org.mozilla.vrbrowser.db.entity.BookmarkEntity; -import org.mozilla.vrbrowser.model.Bookmark; - -import java.util.List; - import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; @@ -16,6 +7,13 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.MediatorLiveData; +import org.mozilla.vrbrowser.browser.engine.SessionStore; +import org.mozilla.vrbrowser.db.AppDatabase; +import org.mozilla.vrbrowser.db.entity.BookmarkEntity; +import org.mozilla.vrbrowser.model.Bookmark; + +import java.util.List; + public class DataRepository implements LifecycleOwner { private static DataRepository sInstance; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java index 23e02dc06..b1e416780 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java @@ -9,6 +9,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; @@ -29,6 +30,10 @@ import android.view.ViewTreeObserver; import android.widget.FrameLayout; +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.geckoview.CrashReporter; import org.mozilla.geckoview.GeckoResult; @@ -37,8 +42,9 @@ import org.mozilla.geckoview.GeckoVRManager; import org.mozilla.vrbrowser.audio.AudioEngine; import org.mozilla.vrbrowser.browser.PermissionDelegate; -import org.mozilla.vrbrowser.browser.SessionStore; import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; +import org.mozilla.vrbrowser.browser.engine.SessionStack; import org.mozilla.vrbrowser.crashreporting.CrashReporterService; import org.mozilla.vrbrowser.crashreporting.GlobalExceptionHandler; import org.mozilla.vrbrowser.geolocation.GeolocationWrapper; @@ -47,13 +53,9 @@ import org.mozilla.vrbrowser.search.SearchEngineWrapper; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.OffscreenDisplay; -import org.mozilla.vrbrowser.ui.widgets.BookmarkListener; -import org.mozilla.vrbrowser.ui.views.BookmarksView; import org.mozilla.vrbrowser.ui.widgets.KeyboardWidget; import org.mozilla.vrbrowser.ui.widgets.NavigationBarWidget; import org.mozilla.vrbrowser.ui.widgets.RootWidget; -import org.mozilla.vrbrowser.ui.widgets.TopBarWidget; -import org.mozilla.vrbrowser.ui.widgets.TrayListener; import org.mozilla.vrbrowser.ui.widgets.TrayWidget; import org.mozilla.vrbrowser.ui.widgets.UIWidget; import org.mozilla.vrbrowser.ui.widgets.VideoProjectionMenuWidget; @@ -61,6 +63,7 @@ import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; import org.mozilla.vrbrowser.ui.widgets.WindowWidget; +import org.mozilla.vrbrowser.ui.widgets.Windows; import org.mozilla.vrbrowser.ui.widgets.dialogs.CrashDialogWidget; import org.mozilla.vrbrowser.utils.ConnectivityReceiver; import org.mozilla.vrbrowser.utils.LocaleUtils; @@ -75,11 +78,7 @@ import java.util.Set; import java.util.function.Consumer; -import androidx.annotation.IntDef; -import androidx.annotation.Keep; -import androidx.annotation.NonNull; - -public class VRBrowserActivity extends PlatformActivity implements WidgetManagerDelegate, SessionStore.VideoAvailabilityListener { +public class VRBrowserActivity extends PlatformActivity implements WidgetManagerDelegate { private BroadcastReceiver mCrashReceiver = new BroadcastReceiver() { @Override @@ -122,16 +121,13 @@ public void run() { SwipeRunnable mLastRunnable; Handler mHandler = new Handler(); Runnable mAudioUpdateRunnable; - WindowWidget mWindowWidget; + Windows mWindows; RootWidget mRootWidget; KeyboardWidget mKeyboard; NavigationBarWidget mNavigationBar; CrashDialogWidget mCrashDialog; - TopBarWidget mTopBar; TrayWidget mTray; - BookmarksView mBookmarksView; PermissionDelegate mPermissionDelegate; - long mExternalContext; LinkedList mWidgetUpdateListeners; LinkedList mPermissionListeners; LinkedList mFocusChangeListeners; @@ -204,8 +200,7 @@ protected void onCreate(Bundle savedInstanceState) { Bundle extras = getIntent() != null ? getIntent().getExtras() : null; SessionStore.get().setContext(this, extras); - SessionStore.get().registerListeners(); - SessionStore.get().addVideoAvailabilityListener(this); + SessionStore.get().initializeStores(this); ((VRBrowserApplication)getApplication()).getRepository().migrateOldBookmarks(); // Create broadcast receiver for getting crash messages from crash process @@ -239,13 +234,15 @@ protected void onCreate(Bundle savedInstanceState) { mSettings = SettingsStore.getInstance(this); - loadFromIntent(getIntent()); queueRunnable(() -> createOffscreenDisplay()); final String tempPath = getCacheDir().getAbsolutePath(); queueRunnable(() -> setTemporaryFilePath(tempPath)); setCylinderDensity(SettingsStore.getInstance(this).getCylinderDensity()); updateFoveatedLevel(); - initializeWorld(); + + initializeWidgets(); + + loadFromIntent(getIntent()); // Setup the search engine mSearchEngineWrapper = SearchEngineWrapper.get(this); @@ -257,31 +254,18 @@ protected void onCreate(Bundle savedInstanceState) { mPoorPerformanceWhiteList = new HashSet<>(); } - protected void initializeWorld() { - // Bookmarks panel - mBookmarksView = new BookmarksView(this); - - // Create browser widget - if (SessionStore.get().getCurrentSession() == null) { - int id = SessionStore.get().createSession(); - SessionStore.get().setCurrentSession(id); - } - int currentSession = SessionStore.get().getCurrentSessionId(); - mWindowWidget = new WindowWidget(this, currentSession); - mWindowWidget.setBookmarksView(mBookmarksView); - mPermissionDelegate.setParentWidgetHandle(mWindowWidget.getHandle()); + protected void initializeWidgets() { + mWindows = new Windows(this); + mWindows.setDelegate(this::attachToWindow); // Create Browser navigation widget mNavigationBar = new NavigationBarWidget(this); - mNavigationBar.setBrowserWidget(mWindowWidget); // Create keyboard widget mKeyboard = new KeyboardWidget(this); - mKeyboard.setBrowserWidget(mWindowWidget); - // Create the top bar - mTopBar = new TopBarWidget(this); - mTopBar.setBrowserWidget(mWindowWidget); + // Create the tray + mTray = new TrayWidget(this); // Empty widget just for handling focus on empty space mRootWidget = new RootWidget(this); @@ -291,14 +275,25 @@ protected void initializeWorld() { } }); - // Create Tray - mTray = new TrayWidget(this); - // Add widget listeners - mTray.addListeners(new TrayListener[]{mWindowWidget, mNavigationBar}); - mBookmarksView.addListeners(new BookmarkListener[]{mWindowWidget, mNavigationBar, mTray}); + mTray.addListeners(mWindows); - addWidgets(Arrays.asList(mRootWidget, mWindowWidget, mNavigationBar, mKeyboard, mTray)); + attachToWindow(mWindows.getFocusedWindow(), null); + + addWidgets(Arrays.asList(mRootWidget, mNavigationBar, mKeyboard, mTray)); + } + + private void attachToWindow(@NonNull WindowWidget aWindow, @Nullable WindowWidget aPrevWindow) { + mPermissionDelegate.setParentWidgetHandle(aWindow.getHandle()); + mNavigationBar.attachToWindow(aWindow); + mKeyboard.attachToWindow(aWindow); + mTray.attachToWindow(aWindow); + + if (aPrevWindow != null) { + updateWidget(mNavigationBar); + updateWidget(mKeyboard); + updateWidget(mTray); + } } @Override @@ -310,7 +305,13 @@ protected void onStart() { @Override protected void onStop() { super.onStop(); + + if (SettingsStore.getInstance(this).getCylinderDensity() > 0.0f) + TelemetryWrapper.queueCurvedModeActiveEvent(); + TelemetryWrapper.stop(); + + mWindows.onStop(); } @Override @@ -321,7 +322,9 @@ protected void onPause() { exitImmersiveSync(); } mAudioEngine.pauseEngine(); - SessionStore.get().setActive(false); + + SessionStore.get().onPause(); + for (Widget widget: mWidgets.values()) { widget.onPause(); } @@ -341,7 +344,9 @@ protected void onResume() { if (mOffscreenDisplay != null) { mOffscreenDisplay.onResume(); } - SessionStore.get().setActive(true); + + SessionStore.get().onResume(); + mAudioEngine.resumeEngine(); for (Widget widget: mWidgets.values()) { widget.onResume(); @@ -372,11 +377,12 @@ protected void onDestroy() { } // Remove all widget listeners + mTray.removeListeners(mWindows); mTray.onDestroy(); - mBookmarksView.onDestroy(); - SessionStore.get().removeVideoAvailabilityListener(this); + mWindows.onDestroy(); + + SessionStore.get().onDestroy(); - SessionStore.get().unregisterListeners(); super.onDestroy(); } @@ -395,6 +401,13 @@ protected void onNewIntent(final Intent intent) { } } + @Override + public void onConfigurationChanged(Configuration newConfig) { + SessionStore.get().onConfigurationChanged(newConfig); + + super.onConfigurationChanged(newConfig); + } + void loadFromIntent(final Intent intent) { if (GeckoRuntime.ACTION_CRASHED.equals(intent.getAction())) { handleCrashIntent(intent); @@ -405,6 +418,8 @@ void loadFromIntent(final Intent intent) { uri = Uri.parse(intent.getExtras().getString("url")); } + SessionStack activeStore = SessionStore.get().getActiveStore(); + Bundle extras = intent.getExtras(); if (extras != null && extras.containsKey("homepage")) { Uri homepageUri = Uri.parse(extras.getString("homepage")); @@ -419,27 +434,31 @@ void loadFromIntent(final Intent intent) { } } - if (SessionStore.get().getCurrentSession() == null) { - String url = (uri != null ? uri.toString() : null); - int id = SessionStore.get().createSession(); - SessionStore.get().setCurrentSession(id); - SessionStore.get().loadUri(url); - Log.d(LOGTAG, "Creating session and loading URI from intent: " + url); - } else if (uri != null) { - Log.d(LOGTAG, "Loading URI from intent: " + uri.toString()); - SessionStore.get().loadUri(uri.toString()); + if (activeStore != null) { + if (activeStore.getCurrentSession() == null) { + String url = (uri != null ? uri.toString() : null); + activeStore.newSessionWithUrl(url); + Log.d(LOGTAG, "Creating session and loading URI from intent: " + url); + + } else if (uri != null) { + Log.d(LOGTAG, "Loading URI from intent: " + uri.toString()); + activeStore.loadUri(uri.toString()); + + } else { + mWindows.getFocusedWindow().loadHomeIfNotRestored(); + } } } private void handleConnectivityChange() { boolean connected = ConnectivityReceiver.isNetworkAvailable(this); - if (connected != mConnectionAvailable && mWindowWidget != null) { - mWindowWidget.setNoInternetToastVisible(!connected); + if (connected != mConnectionAvailable && mWindows.getFocusedWindow() != null) { + mWindows.getFocusedWindow().setNoInternetToastVisible(!connected); } mConnectionAvailable = connected; } - private void handleCrashIntent(final Intent intent) { + private void handleCrashIntent(@NonNull final Intent intent) { Log.e(LOGTAG, "======> Got crashed intent"); Log.d(LOGTAG, "======> Dump File: " + intent.getStringExtra(GeckoRuntime.EXTRA_MINIDUMP_PATH)); @@ -493,16 +512,7 @@ public void onBackPressed() { mBackHandlers.getLast().run(); return; } - if (SessionStore.get().canGoBack()) { - SessionStore.get().goBack(); - - } else if (SessionStore.get().canUnstackSession()){ - SessionStore.get().unstackSession(); - - } else if (SessionStore.get().isCurrentSessionPrivate()) { - SessionStore.get().exitPrivateMode(); - - } else{ + if (!mWindows.handleBack()) { super.onBackPressed(); } } @@ -627,14 +637,27 @@ void handleMotionEvent(final int aHandle, final int aDevice, final boolean aPres if (!isWidgetInputEnabled(widget)) { widget = null; // Fallback to mRootWidget in order to allow world clicks to dismiss UI. } + if (widget instanceof WindowWidget) { + WindowWidget window = (WindowWidget) widget; + boolean focused = mWindows.getFocusedWindow() == window; + if (!focused && aPressed) { + // Do not send hover events to not focused windows. + mWindows.focusWindow(window); + } else if (!focused) { + // Do not send hover events to not focused windows. + widget = null; + } + } + + float scale = widget != null ? widget.getPlacement().textureScale : 1.0f; final float x = aX / scale; final float y = aY / scale; if (widget == null) { MotionEventGenerator.dispatch(mRootWidget, aDevice, aPressed, x, y); - } else if (widget == mWindowWidget && mWindowWidget.getBorderWidth() > 0) { - final int border = mWindowWidget.getBorderWidth(); + } else if (widget.getBorderWidth() > 0) { + final int border = widget.getBorderWidth(); MotionEventGenerator.dispatch(widget, aDevice, aPressed, x - border, y - border); } else { MotionEventGenerator.dispatch(widget, aDevice, aPressed, x, y); @@ -666,11 +689,12 @@ void handleGesture(final int aType) { boolean consumed = false; if ((aType == GestureSwipeLeft) && (mLastGesture == GestureSwipeLeft)) { Log.d(LOGTAG, "Go back!"); - SessionStore.get().goBack(); + SessionStore.get().getActiveStore().goBack(); + consumed = true; } else if ((aType == GestureSwipeRight) && (mLastGesture == GestureSwipeRight)) { Log.d(LOGTAG, "Go forward!"); - SessionStore.get().goForward(); + SessionStore.get().getActiveStore().goForward(); consumed = true; } if (mLastRunnable != null) { @@ -716,7 +740,7 @@ void handleAudioPose(float qx, float qy, float qz, float qw, float px, float py, @SuppressWarnings("unused") void handleResize(final int aHandle, final float aWorldWidth, final float aWorldHeight) { runOnUiThread(() -> { - mWindowWidget.handleResizeEvent(aWorldWidth, aWorldHeight); + mWindows.getFocusedWindow().handleResizeEvent(aWorldWidth, aWorldHeight); }); } @@ -743,11 +767,9 @@ class PauseCompositorRunnable implements Runnable { @Override public void run() { synchronized (VRBrowserActivity.this) { - if (mWindowWidget != null) { - Log.d(LOGTAG, "About to pause Compositor"); - mWindowWidget.pauseCompositor(); - Log.d(LOGTAG, "Compositor Paused"); - } + Log.d(LOGTAG, "About to pause Compositor"); + mWindows.pauseCompositor(); + Log.d(LOGTAG, "Compositor Paused"); done = true; VRBrowserActivity.this.notify(); } @@ -789,10 +811,8 @@ void resumeGeckoViewCompositor() { TelemetryWrapper.uploadImmersiveToHistogram(); Handler handler = new Handler(Looper.getMainLooper()); handler.postDelayed(() -> { - if (mWindowWidget != null) { - mWindowWidget.resumeCompositor(); - Log.d(LOGTAG, "Compositor Resumed"); - } + mWindows.resumeCompositor(); + Log.d(LOGTAG, "Compositor Resumed"); }, 20); } @@ -868,8 +888,8 @@ private void setDeviceType(int aType) { @SuppressWarnings("unused") private void haltActivity(final int aReason) { runOnUiThread(() -> { - if (mConnectionAvailable && mWindowWidget != null) { - mWindowWidget.showAlert(getString(R.string.not_entitled_title), getString(R.string.not_entitled_message, getString(R.string.app_name)), new GeckoSession.PromptDelegate.AlertCallback() { + if (mConnectionAvailable && mWindows.getFocusedWindow() != null) { + mWindows.getFocusedWindow().showAlert(getString(R.string.not_entitled_title), getString(R.string.not_entitled_message, getString(R.string.app_name)), new GeckoSession.PromptDelegate.AlertCallback() { @Override public void dismiss() { VRBrowserActivity.this.finish(); @@ -890,21 +910,22 @@ private void handlePoorPerformance() { if (mIsPresentingImmersive) { return; } - if (mWindowWidget == null) { + WindowWidget window = mWindows.getFocusedWindow(); + if (window == null) { return; } - final String originalUrl = SessionStore.get().getCurrentUri(); + final String originalUrl = window.getSessionStack().getCurrentUri(); if (mPoorPerformanceWhiteList.contains(originalUrl)) { return; } - SessionStore.get().loadUri("about:blank"); + window.getSessionStack().loadUri("about:blank"); final String[] buttons = {getString(R.string.ok_button), null, getString(R.string.performance_unblock_page)}; - mWindowWidget.showButtonPrompt(getString(R.string.performance_title), getString(R.string.performance_message), buttons, new GeckoSession.PromptDelegate.ButtonCallback() { + window.showButtonPrompt(getString(R.string.performance_title), getString(R.string.performance_message), buttons, new GeckoSession.PromptDelegate.ButtonCallback() { @Override public void confirm(int button) { if (button == GeckoSession.PromptDelegate.BUTTON_TYPE_NEGATIVE) { mPoorPerformanceWhiteList.add(originalUrl); - SessionStore.get().loadUri(originalUrl); + window.getSessionStack().loadUri(originalUrl); } } }); @@ -938,7 +959,7 @@ public int newWidgetHandle() { } - public void addWidgets(final Iterable aWidgets) { + public void addWidgets(final Iterable aWidgets) { for (Widget widget: aWidgets) { mWidgets.put(widget.getHandle(), widget); ((View)widget).setVisibility(widget.getPlacement().visible ? View.VISIBLE : View.GONE); @@ -966,12 +987,6 @@ private boolean isWidgetInputEnabled(Widget aWidget) { return mActiveDialog == null || aWidget == null || mActiveDialog == aWidget || aWidget instanceof KeyboardWidget; } - // VideoAvailabilityListener - @Override - public void onVideoAvailabilityChanged(boolean aVideosAvailable) { - queueRunnable(() -> setCPULevelNative(aVideosAvailable ? CPU_LEVEL_HIGH : CPU_LEVEL_NORMAL)); - } - // WidgetManagerDelegate @Override public void addWidget(Widget aWidget) { @@ -1026,8 +1041,13 @@ public void removeWidget(final Widget aWidget) { } @Override - public void startWidgetResize(final Widget aWidget) { - queueRunnable(() -> startWidgetResizeNative(aWidget.getHandle())); + public void updateVisibleWidgets() { + queueRunnable(this::updateVisibleWidgetsNative); + } + + @Override + public void startWidgetResize(final Widget aWidget, float aMaxWidth, float aMaxHeight) { + queueRunnable(() -> startWidgetResizeNative(aWidget.getHandle(), aMaxWidth, aMaxHeight)); } @Override @@ -1169,7 +1189,7 @@ public void setControllersVisible(final boolean aVisible) { @Override public void setWindowSize(float targetWidth, float targetHeight) { - mWindowWidget.resizeByMultiplier(targetWidth / targetHeight, 1.0f); + mWindows.getFocusedWindow().resizeByMultiplier(targetWidth / targetHeight, 1.0f); } @Override @@ -1200,10 +1220,11 @@ public boolean isPermissionGranted(@NonNull String permission) { @Override public void requestPermission(String uri, @NonNull String permission, GeckoSession.PermissionDelegate.Callback aCallback) { + SessionStack activeStore = SessionStore.get().getActiveStore(); if (uri != null && !uri.isEmpty()) { - mPermissionDelegate.onAppPermissionRequest(SessionStore.get().getCurrentSession(), uri, permission, aCallback); + mPermissionDelegate.onAppPermissionRequest(activeStore.getCurrentSession(), uri, permission, aCallback); } else { - mPermissionDelegate.onAndroidPermissionsRequest(SessionStore.get().getCurrentSession(), new String[]{permission}, aCallback); + mPermissionDelegate.onAndroidPermissionsRequest(activeStore.getCurrentSession(), new String[]{permission}, aCallback); } } @@ -1236,12 +1257,28 @@ public void resetUIYaw() { @Override public void setCylinderDensity(final float aDensity) { queueRunnable(() -> setCylinderDensityNative(aDensity)); + if (mWindows != null) { + mWindows.onCurvedModeChanged(); + } + } + + @Override + public void setCPULevel(int aCPULevel) { + queueRunnable(() -> setCPULevelNative(aCPULevel)); + } + + @Override + public void openNewWindow(String uri) { + WindowWidget newWindow = mWindows.addWindow(); + if (newWindow != null) + newWindow.getSessionStack().newSessionWithUrl(uri); } private native void addWidgetNative(int aHandle, WidgetPlacement aPlacement); private native void updateWidgetNative(int aHandle, WidgetPlacement aPlacement); + private native void updateVisibleWidgetsNative(); private native void removeWidgetNative(int aHandle); - private native void startWidgetResizeNative(int aHandle); + private native void startWidgetResizeNative(int aHandle, float maxWidth, float maxHeight); private native void finishWidgetResizeNative(int aHandle); private native void startWidgetMoveNative(int aHandle, int aMoveBehaviour); private native void finishWidgetMoveNative(); @@ -1261,8 +1298,4 @@ public void setCylinderDensity(final float aDensity) { private native void setIsServo(boolean aIsServo); private native void updateFoveatedLevelNative(int appLevel); - @IntDef(value = { CPU_LEVEL_NORMAL, CPU_LEVEL_HIGH}) - private @interface CPULevelFlags {} - private static final int CPU_LEVEL_NORMAL = 0; - private static final int CPU_LEVEL_HIGH = 1; } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/HistoryStore.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/HistoryStore.kt index 2f62c2bda..a8d5655f1 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/HistoryStore.kt +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/HistoryStore.kt @@ -51,7 +51,7 @@ class HistoryStore constructor(val context: Context) { } fun isInHistory(aURL: String): CompletableFuture = GlobalScope.future { - storage.getVisited(listOf(aURL)).size != 0 + storage.getVisited(listOf(aURL)).isNotEmpty() } private fun notifyListeners() { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/PermissionDelegate.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/PermissionDelegate.java index b10a50ea2..6d53358fb 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/PermissionDelegate.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/PermissionDelegate.java @@ -6,17 +6,18 @@ import android.content.pm.PackageManager; import android.util.Log; +import androidx.annotation.NonNull; + import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.PlatformActivity; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.ui.widgets.dialogs.PermissionWidget; +import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; +import org.mozilla.vrbrowser.ui.widgets.dialogs.PermissionWidget; import java.util.ArrayList; import java.util.Arrays; -import androidx.annotation.NonNull; - public class PermissionDelegate implements GeckoSession.PermissionDelegate, WidgetManagerDelegate.PermissionListener { static final int PERMISSION_REQUEST_CODE = 1143; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionChangeListener.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionChangeListener.java new file mode 100644 index 000000000..c253c9623 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionChangeListener.java @@ -0,0 +1,9 @@ +package org.mozilla.vrbrowser.browser; + +import org.mozilla.geckoview.GeckoSession; + +public interface SessionChangeListener { + default void onNewSession(GeckoSession aSession, int aId) {}; + default void onRemoveSession(GeckoSession aSession, int aId) {}; + default void onCurrentSessionChange(GeckoSession aSession, int aId) {}; +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java index 796982a12..d923a828a 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java @@ -64,8 +64,6 @@ SettingsStore getInstance(final @NonNull Context aContext) { public final static int POINTER_COLOR_DEFAULT_DEFAULT = Color.parseColor("#FFFFFF"); public final static int SCROLL_DIRECTION_DEFAULT = 0; public final static String ENV_DEFAULT = "offworld"; - public final static float BROWSER_WORLD_WIDTH_DEFAULT = 4.0f; - public final static float BROWSER_WORLD_HEIGHT_DEFAULT = 2.25f; public final static int MSAA_DEFAULT_LEVEL = 1; public final static boolean AUDIO_ENABLED = false; public final static float CYLINDER_DENSITY_ENABLED_DEFAULT = 4680.0f; @@ -332,28 +330,6 @@ public void setEnvironment(String aEnv) { editor.commit(); } - public float getBrowserWorldWidth() { - return mPrefs.getFloat( - mContext.getString(R.string.settings_key_browser_world_width), BROWSER_WORLD_WIDTH_DEFAULT); - } - - public void setBrowserWorldWidth(float aBrowserWorldWidth) { - SharedPreferences.Editor editor = mPrefs.edit(); - editor.putFloat(mContext.getString(R.string.settings_key_browser_world_width), aBrowserWorldWidth); - editor.commit(); - } - - public float getBrowserWorldHeight() { - return mPrefs.getFloat( - mContext.getString(R.string.settings_key_browser_world_height), BROWSER_WORLD_HEIGHT_DEFAULT); - } - - public void setBrowserWorldHeight(float aBrowserWorldHeight) { - SharedPreferences.Editor editor = mPrefs.edit(); - editor.putFloat(mContext.getString(R.string.settings_key_browser_world_height), aBrowserWorldHeight); - editor.commit(); - } - public int getPointerColor() { return mPrefs.getInt( mContext.getString(R.string.settings_key_pointer_color), POINTER_COLOR_DEFAULT_DEFAULT); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/VideoAvailabilityListener.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/VideoAvailabilityListener.java new file mode 100644 index 000000000..327f5ff50 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/VideoAvailabilityListener.java @@ -0,0 +1,5 @@ +package org.mozilla.vrbrowser.browser; + +public interface VideoAvailabilityListener { + default void onVideoAvailabilityChanged(boolean aVideosAvailable) {}; +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionSettings.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionSettings.java new file mode 100644 index 000000000..896ebe37b --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionSettings.java @@ -0,0 +1,114 @@ +package org.mozilla.vrbrowser.browser.engine; + +import android.content.Context; + +import org.jetbrains.annotations.NotNull; +import org.mozilla.geckoview.GeckoSessionSettings; +import org.mozilla.vrbrowser.browser.SettingsStore; + +class SessionSettings { + + private boolean isMultiprocessEnabled; + private boolean isTrackingProtectionEnabled; + private boolean isSuspendMediaWhenInactiveEnabled; + private int userAgentMode; + private boolean isServoEnabled; + + private SessionSettings(@NotNull Builder builder) { + this.isMultiprocessEnabled = builder.isMltiprocessEnabled; + this.isTrackingProtectionEnabled = builder.isTrackingProtectionEnabled; + this.isSuspendMediaWhenInactiveEnabled = builder.isSuspendMediaWhenInactiveEnabled; + this.userAgentMode = builder.userAgentMode; + this.isServoEnabled = builder.isServoEnabled; + } + + public boolean isMultiprocessEnabled() { + return isMultiprocessEnabled; + } + + public void setMultiprocessEnabled(boolean enabled) { + isMultiprocessEnabled = enabled; + } + + public boolean isTrackingProtectionEnabled() { + return isTrackingProtectionEnabled; + } + + public void setTrackingProtectionEnabled(boolean enabled) { + isTrackingProtectionEnabled = enabled; + } + + public boolean isSuspendMediaWhenInactiveEnabled() { + return isSuspendMediaWhenInactiveEnabled; + } + + public int getUserAgentMode() { + return userAgentMode; + } + + public void setUserAgentMode(int mode) { + userAgentMode = mode; + } + + public boolean isServoEnabled() { + return isServoEnabled; + } + + public void setServoEnabled(boolean enabled) { + isServoEnabled = enabled; + } + + public static class Builder { + + private boolean isMltiprocessEnabled; + private boolean isTrackingProtectionEnabled; + private boolean isSuspendMediaWhenInactiveEnabled; + private int userAgentMode; + private boolean isServoEnabled; + + public Builder() { + } + + public Builder withMultiprocess(boolean isMultiprocessEnabled){ + this.isMltiprocessEnabled = isMultiprocessEnabled; + return this; + } + + public Builder withTrackingProteccion(boolean isTrackingProtectionEnabled){ + this.isTrackingProtectionEnabled = isTrackingProtectionEnabled; + return this; + } + + public Builder withSuspendMediaWhenInactive(boolean isSuspendMediaWhenInactiveEnabled){ + this.isSuspendMediaWhenInactiveEnabled = isSuspendMediaWhenInactiveEnabled; + return this; + } + + public Builder withUserAgent(int userAgent){ + this.userAgentMode = userAgent; + return this; + } + + public Builder withServo(boolean isServoEnabled){ + this.isServoEnabled= isServoEnabled; + return this; + } + + public Builder withDefaultSettings(Context context) { + return new SessionSettings.Builder() + .withMultiprocess(SettingsStore.getInstance(context).isMultiprocessEnabled()) + .withTrackingProteccion(SettingsStore.getInstance(context).isTrackingProtectionEnabled()) + .withSuspendMediaWhenInactive(true) + .withUserAgent(GeckoSessionSettings.USER_AGENT_MODE_VR) + .withServo(false); + } + + public SessionSettings build(){ + SessionSettings settings = new SessionSettings(this); + + return settings; + + } + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionStore.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStack.java similarity index 59% rename from app/src/common/shared/org/mozilla/vrbrowser/browser/SessionStore.java rename to app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStack.java index 05e9acfd9..ad3da4d9b 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionStore.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStack.java @@ -3,148 +3,89 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.vrbrowser.browser; +package org.mozilla.vrbrowser.browser.engine; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; -import org.mozilla.gecko.GeckoProfile; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.mozilla.geckoview.AllowOrDeny; import org.mozilla.geckoview.ContentBlocking; import org.mozilla.geckoview.GeckoResult; import org.mozilla.geckoview.GeckoRuntime; -import org.mozilla.geckoview.GeckoRuntimeSettings; import org.mozilla.geckoview.GeckoSession; -import org.mozilla.geckoview.GeckoSession.SessionState; import org.mozilla.geckoview.GeckoSessionSettings; import org.mozilla.geckoview.MediaElement; -import org.mozilla.geckoview.WebExtension; import org.mozilla.geckoview.WebRequestError; -import org.mozilla.vrbrowser.BuildConfig; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.crashreporting.CrashReporterService; +import org.mozilla.vrbrowser.browser.Media; +import org.mozilla.vrbrowser.browser.SessionChangeListener; +import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.UserAgentOverride; +import org.mozilla.vrbrowser.browser.VideoAvailabilityListener; import org.mozilla.vrbrowser.geolocation.GeolocationData; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.utils.InternalPages; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; -import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import mozilla.components.concept.storage.VisitType; - import static org.mozilla.vrbrowser.utils.ServoUtils.createServoSession; import static org.mozilla.vrbrowser.utils.ServoUtils.isInstanceOfServoSession; import static org.mozilla.vrbrowser.utils.ServoUtils.isServoAvailable; -public class SessionStore implements ContentBlocking.Delegate, GeckoSession.NavigationDelegate, +public class SessionStack implements ContentBlocking.Delegate, GeckoSession.NavigationDelegate, GeckoSession.ProgressDelegate, GeckoSession.ContentDelegate, GeckoSession.TextInputDelegate, GeckoSession.PromptDelegate, GeckoSession.MediaDelegate, SharedPreferences.OnSharedPreferenceChangeListener { - private static SessionStore mInstance; private static final String LOGTAG = "VRB"; - public static SessionStore get() { - if (mInstance == null) { - mInstance = new SessionStore(); - } - return mInstance; - } + // You can test a local file using: "resource://android/assets/webvr/index.html" public static final String PRIVATE_BROWSING_URI = "about:privatebrowsing"; - public static final int NO_SESSION_ID = -1; - private static final String[] WEB_EXTENSIONS = new String[] { - "webcompat_vimeo", - "webcompat_youtube" - }; - - private LinkedList mNavigationListeners; - private LinkedList mProgressListeners; - private LinkedList mContentListeners; - private LinkedList mSessionChangeListeners; - private LinkedList mTextInputListeners; - private LinkedList mPromptListeners; - private LinkedList mVideoAvailabilityListeners; - private UserAgentOverride mUserAgentOverride; - - public interface SessionChangeListener { - void onNewSession(GeckoSession aSession, int aId); - void onRemoveSession(GeckoSession aSession, int aId); - void onCurrentSessionChange(GeckoSession aSession, int aId); - } - - public interface VideoAvailabilityListener { - void onVideoAvailabilityChanged(boolean aVideosAvailable); - } - - class SessionSettings { - boolean multiprocess = SettingsStore.getInstance(mContext).isMultiprocessEnabled(); - boolean privateMode = false; - boolean trackingProtection = SettingsStore.getInstance(mContext).isTrackingProtectionEnabled(); - boolean suspendMediaWhenInactive = true; - int userAgentMode = SettingsStore.getInstance(mContext).getUaMode(); - boolean servo = false; - } - - class State { - boolean mCanGoBack; - boolean mCanGoForward; - boolean mIsLoading; - boolean mIsInputActive; - GeckoSession.ProgressDelegate.SecurityInformation mSecurityInformation; - String mUri; - String mPreviousUri; - String mTitle; - boolean mFullScreen; - GeckoSession mSession; - SessionSettings mSettings; - ArrayList mMediaElements = new ArrayList<>(); - SessionState mSessionState; - } - - private GeckoRuntime mRuntime; - private GeckoSession mCurrentSession; - private HashMap mSessions; + public static final int NO_SESSION = -1; + + private transient LinkedList mNavigationListeners; + private transient LinkedList mProgressListeners; + private transient LinkedList mContentListeners; + private transient LinkedList mSessionChangeListeners; + private transient LinkedList mTextInputListeners; + private transient LinkedList mPromptListeners; + private transient LinkedList mVideoAvailabilityListeners; + private transient UserAgentOverride mUserAgentOverride; + + private transient GeckoSession mCurrentSession; + private HashMap mSessions; private Deque mSessionsStack; - private Deque mPrivateSessionsStack; - private GeckoSession.PermissionDelegate mPermissionDelegate; - private int mPreviousSessionId = SessionStore.NO_SESSION_ID; - private int mPreviousGeckoSessionId = SessionStore.NO_SESSION_ID; + private transient GeckoSession.PermissionDelegate mPermissionDelegate; + private int mPreviousGeckoSessionId = NO_SESSION; private String mRegion; - private Context mContext; - private SharedPreferences mPrefs; - private BookmarksStore mBookmarksStore; - private HistoryStore mHistoryStore; + private transient Context mContext; + private transient SharedPreferences mPrefs; + private transient GeckoRuntime mRuntime; + private boolean mUsePrivateMode; - private SessionStore() { + protected SessionStack(Context context, GeckoRuntime runtime, boolean usePrivateMode) { + mRuntime = runtime; mSessions = new LinkedHashMap<>(); mSessionsStack = new ArrayDeque<>(); - mPrivateSessionsStack = new ArrayDeque<>(); - } + mUsePrivateMode = usePrivateMode; - public void registerListeners() { mNavigationListeners = new LinkedList<>(); mProgressListeners = new LinkedList<>(); mContentListeners = new LinkedList<>(); @@ -156,9 +97,24 @@ public void registerListeners() { if (mPrefs != null) { mPrefs.registerOnSharedPreferenceChangeListener(this); } + + mContext = context; + mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext); + + if (mUserAgentOverride == null) { + mUserAgentOverride = new UserAgentOverride(); + mUserAgentOverride.loadOverridesFromAssets((Activity)mContext, mContext.getString(R.string.user_agent_override_file)); + } } - public void unregisterListeners() { + protected void shutdown() { + for (Map.Entry session : mSessions.entrySet()) { + session.getValue().mSession.close(); + } + + mSessions.clear(); + mSessionsStack.clear(); + mNavigationListeners.clear(); mProgressListeners.clear(); mContentListeners.clear(); @@ -170,71 +126,8 @@ public void unregisterListeners() { mPrefs.unregisterOnSharedPreferenceChangeListener(this); } - if (mBookmarksStore != null) { - mBookmarksStore.removeAllListeners(); - } - if (mHistoryStore!= null) { - mHistoryStore.removeAllListeners(); - } - } - - public void setContext(Context aContext, Bundle aExtras) { - if (mRuntime == null) { - // FIXME: Once GeckoView has a prefs API - vrPrefsWorkAround(aContext, aExtras); - GeckoRuntimeSettings.Builder runtimeSettingsBuilder = new GeckoRuntimeSettings.Builder(); - runtimeSettingsBuilder.crashHandler(CrashReporterService.class); - runtimeSettingsBuilder.contentBlocking((new ContentBlocking.Settings.Builder()) - .categories(ContentBlocking.AT_AD | ContentBlocking.AT_SOCIAL | ContentBlocking.AT_ANALYTIC) - .build()); - runtimeSettingsBuilder.consoleOutput(SettingsStore.getInstance(aContext).isConsoleLogsEnabled()); - runtimeSettingsBuilder.displayDensityOverride(SettingsStore.getInstance(aContext).getDisplayDensity()); - runtimeSettingsBuilder.remoteDebuggingEnabled(SettingsStore.getInstance(aContext).isRemoteDebuggingEnabled()); - runtimeSettingsBuilder.displayDpiOverride(SettingsStore.getInstance(aContext).getDisplayDpi()); - runtimeSettingsBuilder.screenSizeOverride(SettingsStore.getInstance(aContext).getMaxWindowWidth(), - SettingsStore.getInstance(aContext).getMaxWindowHeight()); - if (SettingsStore.getInstance(aContext).getTransparentBorderWidth() > 0) { - runtimeSettingsBuilder.useMaxScreenDepth(true); - } - - if (BuildConfig.DEBUG) { - runtimeSettingsBuilder.arguments(new String[] { "-purgecaches" }); - } - - mRuntime = GeckoRuntime.create(aContext, runtimeSettingsBuilder.build()); - for (String extension: WEB_EXTENSIONS) { - String path = "resource://android/assets/web_extensions/" + extension + "/"; - mRuntime.registerWebExtension(new WebExtension(path)); - } - - } else { - mRuntime.attachTo(aContext); - } - - mContext = aContext; - mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext); - mBookmarksStore = new BookmarksStore(mContext); - mHistoryStore = new HistoryStore(mContext); - if (mUserAgentOverride == null) { - mUserAgentOverride = new UserAgentOverride(); - mUserAgentOverride.loadOverridesFromAssets((Activity)aContext, aContext.getString(R.string.user_agent_override_file)); - } - } - - public BookmarksStore getBookmarkStore() { - return mBookmarksStore; - } - - public HistoryStore getHistoryStore() { - return mHistoryStore; - } - - public void dumpAllState(Integer sessionId) { - dumpAllState(getSession(sessionId)); - } - - private boolean isLocalizedContent(@Nullable String url) { - return url != null && (url.startsWith("about:") || url.startsWith("data:")); + mCurrentSession = null; + mPreviousGeckoSessionId = NO_SESSION; } private void dumpAllState(GeckoSession aSession) { @@ -254,7 +147,7 @@ private void dumpState(GeckoSession aSession, GeckoSession.NavigationDelegate aL boolean canGoBack = false; String uri = ""; if (aSession != null) { - State state = mSessions.get(aSession.hashCode()); + SessionState state = mSessions.get(aSession.hashCode()); if (state != null) { canGoBack = state.mCanGoBack; canGoForward = state.mCanGoForward; @@ -271,7 +164,7 @@ private void dumpState(GeckoSession aSession, GeckoSession.ProgressDelegate aLis GeckoSession.ProgressDelegate.SecurityInformation securityInfo = null; String uri = ""; if (aSession != null) { - State state = mSessions.get(aSession.hashCode()); + SessionState state = mSessions.get(aSession.hashCode()); if (state != null) { isLoading = state.mIsLoading; securityInfo = state.mSecurityInformation; @@ -289,10 +182,10 @@ private void dumpState(GeckoSession aSession, GeckoSession.ProgressDelegate aLis } } - public void dumpState(GeckoSession aSession, GeckoSession.ContentDelegate aListener) { + private void dumpState(GeckoSession aSession, GeckoSession.ContentDelegate aListener) { String title = ""; if (aSession != null) { - State state = mSessions.get(aSession.hashCode()); + SessionState state = mSessions.get(aSession.hashCode()); if (state != null) { title = state.mTitle; } @@ -301,6 +194,13 @@ public void dumpState(GeckoSession aSession, GeckoSession.ContentDelegate aListe aListener.onTitleChange(aSession, title); } + public void setPermissionDelegate(GeckoSession.PermissionDelegate aDelegate) { + mPermissionDelegate = aDelegate; + for (HashMap.Entry entry : mSessions.entrySet()) { + entry.getValue().mSession.setPermissionDelegate(aDelegate); + } + } + public void addNavigationListener(GeckoSession.NavigationDelegate aListener) { mNavigationListeners.add(aListener); dumpState(mCurrentSession, aListener); @@ -360,30 +260,87 @@ public void removeVideoAvailabilityListener(VideoAvailabilityListener aListener) mVideoAvailabilityListeners.remove(aListener); } - public int createSession() { - return createSession(false); - } + public void restore(SessionStack store, int currentSessionId) { + mSessions.clear(); + mSessionsStack.clear(); + + mPreviousGeckoSessionId = store.mPreviousGeckoSessionId; + mRegion = store.mRegion; + mUsePrivateMode = store.mUsePrivateMode; + + HashMap oldNewSessionId = new HashMap<>(); + for (Map.Entry entry : store.mSessions.entrySet()) { + SessionState state = entry.getValue(); + + GeckoSessionSettings geckoSettings = new GeckoSessionSettings.Builder() + .useMultiprocess(state.mSettings.isMultiprocessEnabled()) + .usePrivateMode(mUsePrivateMode) + .userAgentMode(state.mSettings.getUserAgentMode()) + .suspendMediaWhenInactive(state.mSettings.isSuspendMediaWhenInactiveEnabled()) + .useTrackingProtection(state.mSettings.isTrackingProtectionEnabled()) + .build(); + + if (state.mSettings.isServoEnabled()) { + if (isServoAvailable()) { + state.mSession = createServoSession(mContext); + } else { + Log.e(LOGTAG, "Attempt to create a ServoSession. Servo hasn't been enable at build time. Using a GeckoSession instead."); + state.mSession = new GeckoSession(geckoSettings); + } + } else { + state.mSession = new GeckoSession(geckoSettings); + } + + state.mSession.restoreState(state.mSessionState); + + int newSessionId = state.mSession.hashCode(); + oldNewSessionId.put(entry.getKey(), newSessionId); + + state.mSession.setNavigationDelegate(this); + state.mSession.setProgressDelegate(this); + state.mSession.setPromptDelegate(this); + state.mSession.setContentDelegate(this); + state.mSession.getTextInput().setDelegate(this); + state.mSession.setPermissionDelegate(mPermissionDelegate); + state.mSession.setContentBlockingDelegate(this); + state.mSession.setMediaDelegate(this); + for (SessionChangeListener listener: mSessionChangeListeners) { + listener.onNewSession(state.mSession, newSessionId); + } + + mSessions.put(newSessionId, state); + + if (entry.getKey() == currentSessionId) { + setCurrentSession(newSessionId); + } + } - public int createSession(boolean isPrivate) { - SessionStore.SessionSettings settings = new SessionStore.SessionSettings(); - if (isPrivate) { - settings.privateMode = true; + for (Iterator it = store.mSessionsStack.descendingIterator(); it.hasNext();) { + int oldSessionId = it.next(); + int newSessionId = oldNewSessionId.get(oldSessionId); + mSessionsStack.push(newSessionId); } + } + + private int createSession() { + SessionSettings settings = new SessionSettings.Builder() + .withDefaultSettings(mContext) + .build(); return createSession(settings); } - int createSession(SessionSettings aSettings) { - State state = new State(); + private int createSession(@NonNull SessionSettings aSettings) { + SessionState state = new SessionState(); state.mSettings = aSettings; GeckoSessionSettings geckoSettings = new GeckoSessionSettings.Builder() - .useMultiprocess(aSettings.multiprocess) - .usePrivateMode(aSettings.privateMode) - .useTrackingProtection(aSettings.trackingProtection) + .useMultiprocess(aSettings.isMultiprocessEnabled()) + .usePrivateMode(mUsePrivateMode) + .useTrackingProtection(aSettings.isTrackingProtectionEnabled()) .build(); - if (aSettings.servo) { + if (aSettings.isServoEnabled()) { if (isServoAvailable()) { state.mSession = createServoSession(mContext); } else { @@ -397,8 +354,8 @@ int createSession(SessionSettings aSettings) { int result = state.mSession.hashCode(); mSessions.put(result, state); - state.mSession.getSettings().setSuspendMediaWhenInactive(aSettings.suspendMediaWhenInactive); - state.mSession.getSettings().setUserAgentMode(aSettings.userAgentMode); + state.mSession.getSettings().setSuspendMediaWhenInactive(aSettings.isSuspendMediaWhenInactiveEnabled()); + state.mSession.getSettings().setUserAgentMode(aSettings.getUserAgentMode()); state.mSession.setNavigationDelegate(this); state.mSession.setProgressDelegate(this); state.mSession.setPromptDelegate(this); @@ -414,7 +371,27 @@ int createSession(SessionSettings aSettings) { return result; } - public void removeSession(int aSessionId) { + private void recreateSession(SessionSettings aSettings) { + if (mCurrentSession != null) { + SessionState state = mSessions.get(mCurrentSession.hashCode()); + if (state == null) { + return; + } + mCurrentSession.stop(); + mCurrentSession.close(); + + int oldSessionId = getCurrentSessionId(); + int sessionId = createSession(aSettings); + GeckoSession session = getSession(sessionId); + if (state.mSessionState != null) { + session.restoreState(state.mSessionState); + } + setCurrentSession(sessionId); + removeSession(oldSessionId); + } + } + + private void removeSession(int aSessionId) { GeckoSession session = getSession(aSessionId); if (session != null) { session.setContentDelegate(null); @@ -436,52 +413,90 @@ public void removeSession(int aSessionId) { } private void pushSession(int aSessionId) { - boolean isPrivateMode = mCurrentSession.getSettings().getUsePrivateMode(); - if (isPrivateMode) - mPrivateSessionsStack.push(aSessionId); - else - mSessionsStack.push(aSessionId); + mSessionsStack.push(aSessionId); } private Integer popSession() { - boolean isPrivateMode = mCurrentSession.getSettings().getUsePrivateMode(); - if (isPrivateMode) - return mPrivateSessionsStack.pop(); - else - return mSessionsStack.pop(); + Integer sessionId; + try { + sessionId = mSessionsStack.pop(); + + } catch (NoSuchElementException e) { + sessionId = new Integer(NO_SESSION); + } + + return sessionId; } private Integer peekSession() { - boolean isPrivateMode = mCurrentSession.getSettings().getUsePrivateMode(); - if (isPrivateMode) - return mPrivateSessionsStack.peek(); - else - return mSessionsStack.peek(); + Integer sessionId = mSessionsStack.peek(); + return sessionId == null ? NO_SESSION : sessionId; + } + + public void newSession() { + SessionSettings settings = new SessionSettings.Builder().build(); + int id = createSession(settings); + stackSession(id); + } + + public void newSessionWithUrl(String url) { + newSession(); + loadUri(url); + } + + private void stackSession(int sessionId) { + int currentSessionId = getCurrentSessionId(); + if (currentSessionId != NO_SESSION) + pushSession(currentSessionId); + setCurrentSession(sessionId); + + mCurrentSession = null; + SessionState state = mSessions.get(sessionId); + if (state != null) { + mCurrentSession = state.mSession; + for (SessionChangeListener listener : mSessionChangeListeners) { + listener.onCurrentSessionChange(mCurrentSession, sessionId); + } + } + dumpAllState(mCurrentSession); + } + + private void unstackSession() { + Integer prevSessionId = popSession(); + if (prevSessionId != NO_SESSION) { + int currentSession = getCurrentSessionId(); + setCurrentSession(prevSessionId); + removeSession(currentSession); + } } public GeckoSession getSession(int aId) { - State result = mSessions.get(aId); - if (result == null) { + SessionState state = mSessions.get(aId); + if (state == null) { return null; } - return result.mSession; + return state.mSession; + } + + public boolean containsSession(GeckoSession aSession) { + return getSessionId(aSession) != NO_SESSION; } - public Integer getSessionId(GeckoSession aSession) { - for (Map.Entry entry : mSessions.entrySet()) { + private Integer getSessionId(GeckoSession aSession) { + for (Map.Entry entry : mSessions.entrySet()) { if (entry.getValue().mSession == aSession) { return entry.getKey(); } } - return null; + return NO_SESSION; } public String getUriFromSession(GeckoSession aSession) { Integer sessionId = getSessionId(aSession); - if (sessionId == null) { + if (sessionId == NO_SESSION) { return ""; } - State state = mSessions.get(sessionId); + SessionState state = mSessions.get(sessionId); if (state != null) { return state.mUri; } @@ -489,27 +504,7 @@ public String getUriFromSession(GeckoSession aSession) { return ""; } - public List getSessions() { - return new ArrayList<>(mSessions.keySet()); - } - - public List getSessionsByPrivateMode(boolean aUsingPrivateMode) { - ArrayList result = new ArrayList<>(); - for (Integer sessionId : mSessions.keySet()) { - GeckoSession session = getSession(sessionId); - if (session != null && session.getSettings().getUsePrivateMode() == aUsingPrivateMode) { - result.add(sessionId); - } - } - return result; - } - public void setCurrentSession(int aId) { - if (mRuntime == null) { - Log.e(LOGTAG, "SessionStore failed to set current session, GeckoRuntime is null"); - return; - } - Log.d(LOGTAG, "Creating session: " + aId); if (mCurrentSession != null) { @@ -517,7 +512,7 @@ public void setCurrentSession(int aId) { } mCurrentSession = null; - State state = mSessions.get(aId); + SessionState state = mSessions.get(aId); if (state != null) { mCurrentSession = state.mSession; if (!mCurrentSession.isOpen()) { @@ -534,7 +529,7 @@ public void setCurrentSession(int aId) { } public void setRegion(String aRegion) { - Log.d(LOGTAG, "SessionStore setRegion: " + aRegion); + Log.d(LOGTAG, "SessionStack setRegion: " + aRegion); mRegion = aRegion != null ? aRegion.toLowerCase() : "worldwide"; // There is a region initialize and the home is already loaded @@ -560,7 +555,7 @@ public Boolean isHomeUri(String aUri) { public String getCurrentUri() { String result = ""; if (mCurrentSession != null) { - State state = mSessions.get(mCurrentSession.hashCode()); + SessionState state = mSessions.get(mCurrentSession.hashCode()); if (state == null) { return result; } @@ -569,22 +564,10 @@ public String getCurrentUri() { return result; } - public String getPreviousUri() { - String result = ""; - if (mCurrentSession != null) { - State state = mSessions.get(mCurrentSession.hashCode()); - if (state == null) { - return result; - } - result = state.mPreviousUri; - } - return result; - } - public String getCurrentTitle() { String result = ""; if (mCurrentSession != null) { - State state = mSessions.get(mCurrentSession.hashCode()); + SessionState state = mSessions.get(mCurrentSession.hashCode()); if (state == null) { return result; } @@ -595,7 +578,7 @@ public String getCurrentTitle() { public Media getFullScreenVideo() { if (mCurrentSession != null) { - State state = mSessions.get(mCurrentSession.hashCode()); + SessionState state = mSessions.get(mCurrentSession.hashCode()); if (state == null) { return null; } @@ -613,7 +596,7 @@ public Media getFullScreenVideo() { } public boolean isInputActive(int aSessionId) { - SessionStore.State state = mSessions.get(aSessionId); + SessionState state = mSessions.get(aSessionId); if (state != null) { return state.mIsInputActive; } @@ -625,12 +608,14 @@ public boolean canGoBack() { return false; } - State state = mSessions.get(mCurrentSession.hashCode()); + Integer prevSessionId = peekSession(); + SessionState state = mSessions.get(mCurrentSession.hashCode()); + boolean canGoBack = false; if (state != null) { - return state.mCanGoBack; + canGoBack = state.mCanGoBack; } - return false; + return canGoBack || prevSessionId != NO_SESSION; } public void goBack() { @@ -639,8 +624,15 @@ public void goBack() { } if (isInFullScreen()) { exitFullScreen(); + } else { - mCurrentSession.goBack(); + SessionState state = mSessions.get(getCurrentSessionId()); + if (state.mCanGoBack) { + getCurrentSession().goBack(); + + } else { + unstackSession(); + } } } @@ -693,11 +685,13 @@ public void toggleServo() { Log.v("servo", "toggleServo"); if (!isInstanceOfServoSession(mCurrentSession)) { - if (mPreviousGeckoSessionId == SessionStore.NO_SESSION_ID) { + if (mPreviousGeckoSessionId == SessionStack.NO_SESSION) { mPreviousGeckoSessionId = getCurrentSessionId(); String uri = getCurrentUri(); - SessionStore.SessionSettings settings = new SessionStore.SessionSettings(); - settings.servo = true; + SessionSettings settings = new SessionSettings.Builder() + .withDefaultSettings(mContext) + .withServo(true) + .build(); int id = createSession(settings); setCurrentSession(id); loadUri(uri); @@ -707,7 +701,7 @@ public void toggleServo() { } else { removeSession(getCurrentSessionId()); setCurrentSession(mPreviousGeckoSessionId); - mPreviousGeckoSessionId = SessionStore.NO_SESSION_ID; + mPreviousGeckoSessionId = SessionStack.NO_SESSION; } } @@ -716,7 +710,7 @@ public boolean isInFullScreen() { return false; } - State state = mSessions.get(mCurrentSession.hashCode()); + SessionState state = mSessions.get(mCurrentSession.hashCode()); if (state != null) { return state.mFullScreen; } @@ -726,10 +720,10 @@ public boolean isInFullScreen() { public boolean isInFullScreen(GeckoSession aSession) { Integer sessionId = getSessionId(aSession); - if (sessionId == null) { + if (sessionId == NO_SESSION) { return false; } - State state = mSessions.get(sessionId); + SessionState state = mSessions.get(sessionId); if (state != null) { return state.mFullScreen; } @@ -750,259 +744,65 @@ public GeckoSession getCurrentSession() { public int getCurrentSessionId() { if (mCurrentSession == null) { - return NO_SESSION_ID; + return NO_SESSION; } return mCurrentSession.hashCode(); } - public void setPermissionDelegate(GeckoSession.PermissionDelegate aDelegate) { - mPermissionDelegate = aDelegate; - for (HashMap.Entry entry : mSessions.entrySet()) { - entry.getValue().mSession.setPermissionDelegate(aDelegate); - } - } - - private void vrPrefsWorkAround(Context aContext, Bundle aExtras) { - File path = GeckoProfile.initFromArgs(aContext, null).getDir(); - String prefFileName = path.getAbsolutePath() + File.separator + "user.js"; - Log.i(LOGTAG, "Creating file: " + prefFileName); - try (FileOutputStream out = new FileOutputStream(prefFileName)) { - out.write("pref(\"dom.vr.enabled\", true);\n".getBytes()); - out.write("pref(\"dom.vr.external.enabled\", true);\n".getBytes()); - out.write("pref(\"webgl.enable-surface-texture\", true);\n".getBytes()); - // Enable MultiView draft extension - out.write("pref(\"webgl.enable-draft-extensions\", true);\n".getBytes()); - out.write("pref(\"apz.allow_double_tap_zooming\", false);\n".getBytes()); - out.write("pref(\"dom.webcomponents.customelements.enabled\", true);\n".getBytes()); - out.write("pref(\"javascript.options.ion\", true);\n".getBytes()); - out.write("pref(\"media.webspeech.synth.enabled\", false);\n".getBytes()); - // Prevent autozoom when giving a form field focus. - out.write("pref(\"formhelper.autozoom\", false);\n".getBytes()); - // Uncomment this to enable WebRender. WARNING NOT READY FOR USAGE. - // out.write("pref(\"gfx.webrender.all\", true);\n".getBytes()); - int msaa = SettingsStore.getInstance(aContext).getMSAALevel(); - if (msaa > 0) { - int msaaLevel = msaa == 2 ? 4 : 2; - out.write(("pref(\"gl.msaa-level\"," + msaaLevel + ");\n").getBytes()); - } - addOptionalPref(out, "dom.vr.require-gesture", aExtras); - addOptionalPref(out, "privacy.reduceTimerPrecision", aExtras); - if (aExtras != null && aExtras.getBoolean("media.autoplay.enabled", false)) { - // Enable playing audios without gesture (used for gfx automated testing) - out.write("pref(\"media.autoplay.enabled.user-gestures-needed\", false);\n".getBytes()); - out.write("pref(\"media.autoplay.enabled.ask-permission\", false);\n".getBytes()); - out.write("pref(\"media.autoplay.default\", 0);\n".getBytes()); - } - } catch (FileNotFoundException e) { - Log.e(LOGTAG, "Unable to create file: '" + prefFileName + "' got exception: " + e.toString()); - } catch (IOException e) { - Log.e(LOGTAG, "Unable to write file: '" + prefFileName + "' got exception: " + e.toString()); - } - } - - private void addOptionalPref(FileOutputStream out, String aKey, Bundle aExtras) throws IOException { - if (aExtras != null && aExtras.containsKey(aKey)) { - boolean value = aExtras.getBoolean(aKey); - out.write(String.format("pref(\"%s\", %s);\n", aKey, value ? "true" : "false").getBytes()); - } - } - - public void switchPrivateMode() { - if (mCurrentSession == null) - return; - - boolean isPrivateMode = mCurrentSession.getSettings().getUsePrivateMode(); - if (!isPrivateMode) { - if (mPreviousSessionId == SessionStore.NO_SESSION_ID) { - mPreviousSessionId = getCurrentSessionId(); - - SessionStore.SessionSettings settings = new SessionStore.SessionSettings(); - settings.privateMode = true; - int id = createSession(settings); - setCurrentSession(id); - - InternalPages.PageResources pageResources = InternalPages.PageResources.create(R.raw.private_mode, R.raw.private_style); - getCurrentSession().loadData(InternalPages.createAboutPage(mContext, pageResources), "text/html"); - - } else { - int sessionId = getCurrentSessionId(); - setCurrentSession(mPreviousSessionId); - mPreviousSessionId = sessionId; - } - - } else { - int sessionId = getCurrentSessionId(); - setCurrentSession(mPreviousSessionId); - mPreviousSessionId = sessionId; - } - } - - public int getUaMode() { - return mCurrentSession.getSettings().getUserAgentMode(); - } - - public void setUaMode(int mode) { - if (mCurrentSession != null) { - mCurrentSession.getSettings().setUserAgentMode(mode); - mCurrentSession.reload(); - } - } - - public void exitPrivateMode() { - if (mCurrentSession == null) - return; - - boolean isPrivateMode = mCurrentSession.getSettings().getUsePrivateMode(); - if (isPrivateMode) { - int privateSessionId = getCurrentSessionId(); - setCurrentSession(mPreviousSessionId); - mPreviousSessionId = SessionStore.NO_SESSION_ID; - - // Remove current private_mode session - removeSession(privateSessionId); - - // Remove all the stacked private_mode sessions - for (Iterator it = mPrivateSessionsStack.iterator(); it.hasNext();) { - int sessionId = it.next(); - removeSession(sessionId); - } - mPrivateSessionsStack.clear(); - } - } - - public boolean isCurrentSessionPrivate() { + public boolean isPrivateMode() { if (mCurrentSession != null) return mCurrentSession.getSettings().getUsePrivateMode(); return false; } - public boolean canUnstackSession() { - Integer prevSessionId = peekSession(); - - return prevSessionId != null; - } - - public void stackSession(int sessionId) { - pushSession(getCurrentSessionId()); - setCurrentSession(sessionId); + // Session Settings - mCurrentSession = null; - State state = mSessions.get(sessionId); + protected void setServo(final boolean enabled) { + SessionState state = mSessions.get(mCurrentSession.hashCode()); if (state != null) { - mCurrentSession = state.mSession; - for (SessionChangeListener listener : mSessionChangeListeners) { - listener.onCurrentSessionChange(mCurrentSession, sessionId); - } + state.mSettings.setServoEnabled(enabled); } - dumpAllState(mCurrentSession); - } - - public void unstackSession() { - Integer prevSessionId = popSession(); - if (prevSessionId != null) { - int currentSession = getCurrentSessionId(); - setCurrentSession(prevSessionId); - removeSession(currentSession); - } - } - - public void setConsoleOutputEnabled(boolean enabled) { - if (mRuntime != null) { - mRuntime.getSettings().setConsoleOutputEnabled(enabled); + if (!enabled && mCurrentSession != null && isInstanceOfServoSession(mCurrentSession)) { + String uri = getCurrentUri(); + int id = createSession(); + setCurrentSession(id); + loadUri(uri); } } - public void setMaxWindowSize(int width, int height) { - SharedPreferences.Editor editor = mPrefs.edit(); - editor.putInt( - mContext.getString(R.string.settings_key_max_window_height), - height); - editor.putInt( - mContext.getString(R.string.settings_key_max_window_width), - width); - editor.commit(); - } - - public void setServo(final boolean enabled) { - if (!enabled && mCurrentSession != null && isInstanceOfServoSession(mCurrentSession)) { - String uri = getCurrentUri(); - int id = createSession(); - setCurrentSession(id); - loadUri(uri); - } + public int getUaMode() { + return mCurrentSession.getSettings().getUserAgentMode(); } - private void recreateSession(SessionStore.SessionSettings aSettings) { + public void setUaMode(int mode) { if (mCurrentSession != null) { - State state = mSessions.get(mCurrentSession.hashCode()); - if (state == null) { - return; - } - mCurrentSession.stop(); - mCurrentSession.close(); - - int oldSessionId = getCurrentSessionId(); - int sessionId = createSession(aSettings); - GeckoSession session = getSession(sessionId); - if (state.mSessionState != null) { - session.restoreState(state.mSessionState); + SessionState state = mSessions.get(mCurrentSession.hashCode()); + if (state != null && state.mSettings.getUserAgentMode() != mode) { + state.mSettings.setUserAgentMode(mode); + mCurrentSession.getSettings().setUserAgentMode(mode); + mCurrentSession.reload(); } - setCurrentSession(sessionId); - removeSession(oldSessionId); } } - private State getCurrentState() { + protected void setMultiprocess(final boolean aEnabled) { if (mCurrentSession != null) { - return mSessions.get(mCurrentSession.hashCode()); - } - return null; - } - - public void setMultiprocess(final boolean aEnabled) { - State state = getCurrentState(); - if (state != null && state.mSettings.multiprocess != aEnabled) { - state.mSettings.multiprocess = aEnabled; - recreateSession(state.mSettings); - } - } - - public void setTrackingProtection(final boolean aEnabled) { - State state = getCurrentState(); - if (state != null && state.mSettings.trackingProtection != aEnabled) { - state.mSettings.trackingProtection = aEnabled; - recreateSession(state.mSettings); - } - } - - public void setRemoteDebugging(final boolean enabled) { - if (mRuntime != null) { - mRuntime.getSettings().setRemoteDebuggingEnabled(enabled); - } - } - - public void setAutoplayEnabled(final boolean enabled) { - if (mRuntime != null) { - mRuntime.getSettings().setAutoplayDefault(enabled ? - GeckoRuntimeSettings.AUTOPLAY_DEFAULT_ALLOWED : - GeckoRuntimeSettings.AUTOPLAY_DEFAULT_BLOCKED); - } - } - - public boolean getAutoplayEnabled() { - if (mRuntime != null) { - return mRuntime.getSettings().getAutoplayDefault() == GeckoRuntimeSettings.AUTOPLAY_DEFAULT_ALLOWED ? - true : - false; + SessionState state = mSessions.get(mCurrentSession.hashCode()); + if (state != null && state.mSettings.isMultiprocessEnabled() != aEnabled) { + state.mSettings.setMultiprocessEnabled(aEnabled); + recreateSession(state.mSettings); + } } - - return false; } - public void setLocales(List locales) { - if (mRuntime != null) { - mRuntime.getSettings().setLocales(locales.stream().toArray(String[]::new)); + protected void setTrackingProtection(final boolean aEnabled) { + if (mCurrentSession != null) { + SessionState state = mSessions.get(mCurrentSession.hashCode()); + if (state != null && state.mSettings.isTrackingProtectionEnabled() != aEnabled) { + state.mSettings.setTrackingProtectionEnabled(aEnabled); + recreateSession(state.mSettings); + } } } @@ -1010,8 +810,8 @@ public void setLocales(List locales) { @Override public void onLocationChange(@NonNull GeckoSession aSession, String aUri) { - Log.d(LOGTAG, "SessionStore onLocationChange: " + aUri); - State state = mSessions.get(aSession.hashCode()); + Log.d(LOGTAG, "SessionStack onLocationChange: " + aUri); + SessionState state = mSessions.get(aSession.hashCode()); if (state == null) { Log.e(LOGTAG, "Unknown session!"); return; @@ -1034,8 +834,8 @@ public void onLocationChange(@NonNull GeckoSession aSession, String aUri) { @Override public void onCanGoBack(@NonNull GeckoSession aSession, boolean aCanGoBack) { - Log.d(LOGTAG, "SessionStore onCanGoBack: " + (aCanGoBack ? "true" : "false")); - State state = mSessions.get(aSession.hashCode()); + Log.d(LOGTAG, "SessionStack onCanGoBack: " + (aCanGoBack ? "true" : "false")); + SessionState state = mSessions.get(aSession.hashCode()); if (state == null) { return; } @@ -1050,8 +850,8 @@ public void onCanGoBack(@NonNull GeckoSession aSession, boolean aCanGoBack) { @Override public void onCanGoForward(@NonNull GeckoSession aSession, boolean aCanGoForward) { - Log.d(LOGTAG, "SessionStore onCanGoForward: " + (aCanGoForward ? "true" : "false")); - State state = mSessions.get(aSession.hashCode()); + Log.d(LOGTAG, "SessionStack onCanGoForward: " + (aCanGoForward ? "true" : "false")); + SessionState state = mSessions.get(aSession.hashCode()); if (state == null) { return; } @@ -1066,22 +866,14 @@ public void onCanGoForward(@NonNull GeckoSession aSession, boolean aCanGoForward @Override public @Nullable GeckoResult onLoadRequest(@NonNull GeckoSession aSession, @NonNull LoadRequest aRequest) { - if (aRequest.isRedirect) - mHistoryStore.addHistory(aRequest.uri, VisitType.EMBED); - else if (aRequest.triggerUri != null) - mHistoryStore.addHistory(aRequest.uri, VisitType.LINK); - - final GeckoResult result = new GeckoResult<>(); - String uri = aRequest.uri; Log.d(LOGTAG, "onLoadRequest: " + uri); - String uriOverride = checkYoutubeOverride(uri); + String uriOverride = SessionUtils.checkYoutubeOverride(uri); if (uriOverride != null) { aSession.loadUri(uriOverride); - result.complete(AllowOrDeny.DENY); - return result; + return GeckoResult.DENY; } if (aSession == mCurrentSession) { @@ -1090,11 +882,14 @@ else if (aRequest.triggerUri != null) } if (PRIVATE_BROWSING_URI.equalsIgnoreCase(uri)) { - switchPrivateMode(); - result.complete(AllowOrDeny.ALLOW); - return result; + return GeckoResult.DENY; } + if (mNavigationListeners.size() == 0) { + return GeckoResult.ALLOW; + } + + final GeckoResult result = new GeckoResult<>(); AtomicInteger count = new AtomicInteger(0); AtomicBoolean allowed = new AtomicBoolean(false); for (GeckoSession.NavigationDelegate listener: mNavigationListeners) { @@ -1114,74 +909,21 @@ else if (aRequest.triggerUri != null) return result; } - /** - * 1. Disable YouTube's Polymer layout (which makes YouTube very slow in non-Chrome browsers) - * via a query-string parameter in the URL. - * 2. Rewrite YouTube URLs from `m.youtube.com` -> `youtube.com` (to avoid serving YouTube's - * video pages intended for mobile phones, as linked from Google search results). - */ - private String checkYoutubeOverride(String aUri) { - try { - Uri uri = Uri.parse(aUri); - if (uri.getHost() == null) { - return null; - } - String hostLower = uri.getHost().toLowerCase(); - if (!hostLower.endsWith(".youtube.com") && !hostLower.endsWith(".youtube-nocookie.com")) { - return null; - } - - Uri.Builder uriBuilder = uri.buildUpon(); - boolean updateUri = false; - - if (uri.getScheme() == null) { - return null; - } - if (!uri.getScheme().equalsIgnoreCase("https")) { - uriBuilder.scheme("https"); - updateUri = true; - } - if (hostLower.startsWith("m.")) { - uriBuilder.authority(hostLower.replaceFirst("m.", "www.")); - updateUri = true; - } - String queryDisablePolymer = uri.getQueryParameter("disable_polymer"); - if (queryDisablePolymer == null) { - uriBuilder.appendQueryParameter("disable_polymer", "1"); - updateUri = true; - } - - if (!updateUri) { - return null; - } - - return uriBuilder.build().toString(); - } catch (Exception ex) { - Log.e(LOGTAG, "Unable to construct transformed URL: " + ex.toString()); - } - - return null; - } - @Override public GeckoResult onNewSession(@NonNull GeckoSession aSession, @NonNull String aUri) { - Log.d(LOGTAG, "SessionStore onNewSession: " + aUri); + Log.d(LOGTAG, "SessionStack onNewSession: " + aUri); pushSession(getCurrentSessionId()); int sessionId; boolean isPreviousPrivateMode = mCurrentSession.getSettings().getUsePrivateMode(); - if (isPreviousPrivateMode) { - SessionStore.SessionSettings settings = new SessionStore.SessionSettings(); - settings.privateMode = true; - sessionId = createSession(settings); - - } else { - sessionId = createSession(); - } + SessionSettings settings = new SessionSettings.Builder() + .withDefaultSettings(mContext) + .build(); + sessionId = createSession(settings); mCurrentSession = null; - State state = mSessions.get(sessionId); + SessionState state = mSessions.get(sessionId); if (state != null) { mCurrentSession = state.mSession; @@ -1198,7 +940,7 @@ public GeckoResult onNewSession(@NonNull GeckoSession aSession, @N @Override public GeckoResult onLoadError(@NonNull GeckoSession session, String uri, @NonNull WebRequestError error) { - Log.d(LOGTAG, "SessionStore onLoadError: " + uri); + Log.d(LOGTAG, "SessionStack onLoadError: " + uri); return GeckoResult.fromValue(InternalPages.createErrorPage(mContext, uri, error.category, error.code)); } @@ -1207,8 +949,8 @@ public GeckoResult onLoadError(@NonNull GeckoSession session, String uri @Override public void onPageStart(@NonNull GeckoSession aSession, @NonNull String aUri) { - Log.d(LOGTAG, "SessionStore onPageStart"); - State state = mSessions.get(aSession.hashCode()); + Log.d(LOGTAG, "SessionStack onPageStart"); + SessionState state = mSessions.get(aSession.hashCode()); if (state == null) { return; } @@ -1224,14 +966,14 @@ public void onPageStart(@NonNull GeckoSession aSession, @NonNull String aUri) { @Override public void onPageStop(@NonNull GeckoSession aSession, boolean b) { - Log.d(LOGTAG, "SessionStore onPageStop"); - State state = mSessions.get(aSession.hashCode()); + Log.d(LOGTAG, "SessionStack onPageStop"); + SessionState state = mSessions.get(aSession.hashCode()); if (state == null) { return; } state.mIsLoading = false; - if (!isLocalizedContent(state.mUri)) { + if (!SessionUtils.isLocalizedContent(state.mUri)) { TelemetryWrapper.uploadPageLoadToHistogram(state.mUri); } @@ -1242,15 +984,10 @@ public void onPageStop(@NonNull GeckoSession aSession, boolean b) { } } - @Override - public void onProgressChange(@NonNull GeckoSession session, int progress) { - - } - @Override public void onSecurityChange(@NonNull GeckoSession aSession, @NonNull SecurityInformation aInformation) { - Log.d(LOGTAG, "SessionStore onPageStop"); - State state = mSessions.get(aSession.hashCode()); + Log.d(LOGTAG, "SessionStack onPageStop"); + SessionState state = mSessions.get(aSession.hashCode()); if (state == null) { return; } @@ -1266,8 +1003,8 @@ public void onSecurityChange(@NonNull GeckoSession aSession, @NonNull SecurityIn @Override public void onSessionStateChange(@NonNull GeckoSession aSession, - @NonNull SessionState aSessionState) { - State state = mSessions.get(aSession.hashCode()); + @NonNull GeckoSession.SessionState aSessionState) { + SessionState state = mSessions.get(aSession.hashCode()); if (state != null) { state.mSessionState = aSessionState; } @@ -1277,8 +1014,8 @@ public void onSessionStateChange(@NonNull GeckoSession aSession, @Override public void onTitleChange(@NonNull GeckoSession aSession, String aTitle) { - Log.d(LOGTAG, "SessionStore onTitleChange"); - State state = mSessions.get(aSession.hashCode()); + Log.d(LOGTAG, "SessionStack onTitleChange"); + SessionState state = mSessions.get(aSession.hashCode()); if (state == null) { return; } @@ -1292,11 +1029,6 @@ public void onTitleChange(@NonNull GeckoSession aSession, String aTitle) { } } - @Override - public void onFocusRequest(@NonNull GeckoSession aSession) { - Log.d(LOGTAG, "SessionStore onFocusRequest"); - } - @Override public void onCloseRequest(@NonNull GeckoSession aSession) { int sessionId = getSessionId(aSession); @@ -1307,8 +1039,8 @@ public void onCloseRequest(@NonNull GeckoSession aSession) { @Override public void onFullScreen(@NonNull GeckoSession aSession, boolean aFullScreen) { - Log.d(LOGTAG, "SessionStore onFullScreen"); - State state = mSessions.get(aSession.hashCode()); + Log.d(LOGTAG, "SessionStack onFullScreen"); + SessionState state = mSessions.get(aSession.hashCode()); if (state == null) { return; } @@ -1322,19 +1054,18 @@ public void onFullScreen(@NonNull GeckoSession aSession, boolean aFullScreen) { } @Override - public void onContextMenu(@NonNull GeckoSession aSession, int i, int i1, @NonNull ContextElement element) { - - } - - @Override - public void onExternalResponse(@NonNull GeckoSession session, @NonNull GeckoSession.WebResponseInfo response) { - + public void onContextMenu(@NonNull GeckoSession session, int screenX, int screenY, @NonNull ContextElement element) { + if (mCurrentSession == session) { + for (GeckoSession.ContentDelegate listener : mContentListeners) { + listener.onContextMenu(session, screenX, screenY, element); + } + } } @Override public void onCrash(@NonNull GeckoSession session) { Log.e(LOGTAG,"Child crashed. Creating new session"); - int crashedSessionId = SessionStore.get().getCurrentSessionId(); + int crashedSessionId = getCurrentSessionId(); int newSessionId = createSession(); setCurrentSession(newSessionId); loadUri(getHomeUri()); @@ -1363,7 +1094,7 @@ public void restartInput(@NonNull GeckoSession aSession, int reason) { @Override public void showSoftInput(@NonNull GeckoSession aSession) { - SessionStore.State state = mSessions.get(getSessionId(aSession)); + SessionState state = mSessions.get(getSessionId(aSession)); if (state != null) { state.mIsInputActive = true; } @@ -1376,7 +1107,7 @@ public void showSoftInput(@NonNull GeckoSession aSession) { @Override public void hideSoftInput(@NonNull GeckoSession aSession) { - SessionStore.State state = mSessions.get(getSessionId(aSession)); + SessionState state = mSessions.get(getSessionId(aSession)); if (state != null) { state.mIsInputActive = false; } @@ -1414,11 +1145,6 @@ public void updateCursorAnchorInfo(@NonNull GeckoSession aSession, @NonNull Curs } } - @Override - public void notifyAutoFill(@NonNull GeckoSession session, int notification, int virtualId) { - - } - @Override public void onContentBlocked(@NonNull final GeckoSession session, @NonNull final ContentBlocking.BlockEvent event) { if ((event.categories & ContentBlocking.AT_AD) != 0) { @@ -1521,7 +1247,7 @@ public GeckoResult onPopupRequest(@NonNull final GeckoSession sessi @Override public void onMediaAdd(@NonNull GeckoSession session, @NonNull MediaElement element) { - SessionStore.State state = mSessions.get(getSessionId(session)); + SessionState state = mSessions.get(getSessionId(session)); if (state == null) { return; } @@ -1537,7 +1263,7 @@ public void onMediaAdd(@NonNull GeckoSession session, @NonNull MediaElement elem @Override public void onMediaRemove(@NonNull GeckoSession session, @NonNull MediaElement element) { - SessionStore.State state = mSessions.get(getSessionId(session)); + SessionState state = mSessions.get(getSessionId(session)); if (state == null) { return; } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionState.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionState.java new file mode 100644 index 000000000..f1bf4cb2c --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionState.java @@ -0,0 +1,47 @@ +package org.mozilla.vrbrowser.browser.engine; + +import com.google.gson.JsonParser; +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import org.json.JSONException; +import org.mozilla.geckoview.GeckoSession; +import org.mozilla.vrbrowser.browser.Media; + +import java.io.IOException; +import java.util.ArrayList; + +public class SessionState { + + public boolean mCanGoBack; + public boolean mCanGoForward; + public boolean mIsLoading; + public boolean mIsInputActive; + public transient GeckoSession.ProgressDelegate.SecurityInformation mSecurityInformation; + public String mUri; + public String mPreviousUri; + public String mTitle; + public boolean mFullScreen; + public transient GeckoSession mSession; + public SessionSettings mSettings; + public transient ArrayList mMediaElements = new ArrayList<>(); + @JsonAdapter(SessionState.GeckoSessionStateAdapter.class) + public GeckoSession.SessionState mSessionState; + + public class GeckoSessionStateAdapter extends TypeAdapter { + @Override public void write(JsonWriter out, GeckoSession.SessionState session) throws IOException { + out.jsonValue(session.toString()); + } + @Override public GeckoSession.SessionState read(JsonReader in) { + try { + String session = new JsonParser().parse(in).toString(); + return GeckoSession.SessionState.fromString(session); + + } catch (JSONException e) { + return null; + } + } + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java new file mode 100644 index 000000000..d165d8b67 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java @@ -0,0 +1,259 @@ +package org.mozilla.vrbrowser.browser.engine; + +import android.content.Context; +import android.content.res.Configuration; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.mozilla.geckoview.ContentBlocking; +import org.mozilla.geckoview.GeckoRuntime; +import org.mozilla.geckoview.GeckoRuntimeSettings; +import org.mozilla.geckoview.GeckoSession; +import org.mozilla.geckoview.WebExtension; +import org.mozilla.vrbrowser.BuildConfig; +import org.mozilla.vrbrowser.browser.BookmarksStore; +import org.mozilla.vrbrowser.browser.HistoryStore; +import org.mozilla.vrbrowser.browser.PermissionDelegate; +import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.crashreporting.CrashReporterService; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SessionStore implements GeckoSession.PermissionDelegate { + + public final int NO_ACTIVE_STORE_ID = -1; + + private static final String[] WEB_EXTENSIONS = new String[] { + "webcompat_vimeo", + "webcompat_youtube" + }; + + private static SessionStore mInstance; + + public static SessionStore get() { + if (mInstance == null) { + mInstance = new SessionStore(); + } + return mInstance; + } + + private Context mContext; + private GeckoRuntime mRuntime; + private HashMap mSessionStacks; + private Integer mActiveStoreId; + private PermissionDelegate mPermissionDelegate; + private BookmarksStore mBookmarksStore; + private HistoryStore mHistoryStore; + + private SessionStore() { + mSessionStacks = new HashMap<>(); + mActiveStoreId = NO_ACTIVE_STORE_ID; + } + + public void setContext(Context context, Bundle aExtras) { + mContext = context; + + if (mRuntime == null) { + // FIXME: Once GeckoView has a prefs API + SessionUtils.vrPrefsWorkAround(context, aExtras); + + GeckoRuntimeSettings.Builder runtimeSettingsBuilder = new GeckoRuntimeSettings.Builder(); + runtimeSettingsBuilder.crashHandler(CrashReporterService.class); + runtimeSettingsBuilder.contentBlocking((new ContentBlocking.Settings.Builder()) + .categories(ContentBlocking.AT_AD | ContentBlocking.AT_SOCIAL | ContentBlocking.AT_ANALYTIC) + .build()); + runtimeSettingsBuilder.consoleOutput(SettingsStore.getInstance(context).isConsoleLogsEnabled()); + runtimeSettingsBuilder.displayDensityOverride(SettingsStore.getInstance(context).getDisplayDensity()); + runtimeSettingsBuilder.remoteDebuggingEnabled(SettingsStore.getInstance(context).isRemoteDebuggingEnabled()); + runtimeSettingsBuilder.displayDpiOverride(SettingsStore.getInstance(context).getDisplayDpi()); + runtimeSettingsBuilder.screenSizeOverride(SettingsStore.getInstance(context).getMaxWindowWidth(), + SettingsStore.getInstance(context).getMaxWindowHeight()); + + if (SettingsStore.getInstance(context).getTransparentBorderWidth() > 0) { + runtimeSettingsBuilder.useMaxScreenDepth(true); + } + + if (BuildConfig.DEBUG) { + runtimeSettingsBuilder.arguments(new String[] { "-purgecaches" }); + } + + mRuntime = GeckoRuntime.create(context, runtimeSettingsBuilder.build()); + for (String extension: WEB_EXTENSIONS) { + String path = "resource://android/assets/web_extensions/" + extension + "/"; + mRuntime.registerWebExtension(new WebExtension(path)); + } + + } else { + mRuntime.attachTo(context); + } + } + + public void initializeStores(Context context) { + mBookmarksStore = new BookmarksStore(context); + mHistoryStore = new HistoryStore(context); + } + + public SessionStack createSessionStack(int storeId, boolean privateMode) { + SessionStack store = new SessionStack(mContext, mRuntime, privateMode); + store.setPermissionDelegate(this); + mSessionStacks.put(storeId, store); + + return store; + } + + public void destroySessionStack(int storeId) { + SessionStack store = mSessionStacks.remove(storeId); + if (store != null) { + store.setPermissionDelegate(null); + store.shutdown(); + } + } + + public void setActiveStore(int storeId) { + mActiveStoreId = storeId; + } + + public SessionStack getSessionStack(int storeId) { + return mSessionStacks.get(storeId); + } + + public SessionStack getActiveStore() { + return mSessionStacks.get(mActiveStoreId); + } + + public void setPermissionDelegate(PermissionDelegate delegate) { + mPermissionDelegate = delegate; + } + + public BookmarksStore getBookmarkStore() { + return mBookmarksStore; + } + + public HistoryStore getHistoryStore() { + return mHistoryStore; + } + + public void onPause() { + for (Map.Entry entry : mSessionStacks.entrySet()) { + entry.getValue().setActive(false); + } + } + + public void onResume() { + for (Map.Entry entry : mSessionStacks.entrySet()) { + entry.getValue().setActive(true); + } + } + + public void onDestroy() { + HashMap sessionStacks = new HashMap<>(mSessionStacks); + for (Map.Entry entry : sessionStacks.entrySet()) { + destroySessionStack(entry.getKey()); + } + + if (mBookmarksStore != null) { + mBookmarksStore.removeAllListeners(); + } + + if (mHistoryStore != null) { + mHistoryStore.removeAllListeners(); + } + } + + public void onConfigurationChanged(Configuration newConfig) { + if (mRuntime != null) { + mRuntime.configurationChanged(newConfig); + } + } + + // Session Settings + + public void setServo(final boolean enabled) { + for (Map.Entry entry : mSessionStacks.entrySet()) { + entry.getValue().setServo(enabled); + } + } + + public void setUaMode(final int mode) { + for (Map.Entry entry : mSessionStacks.entrySet()) { + entry.getValue().setUaMode(mode); + } + } + + public void setMultiprocess(final boolean aEnabled) { + for (Map.Entry entry : mSessionStacks.entrySet()) { + entry.getValue().setMultiprocess(aEnabled); + } + } + + public void setTrackingProtection(final boolean aEnabled) { + for (Map.Entry entry : mSessionStacks.entrySet()) { + entry.getValue().setTrackingProtection(aEnabled); + } + } + + // Runtime Settings + + public void setConsoleOutputEnabled(boolean enabled) { + if (mRuntime != null) { + mRuntime.getSettings().setConsoleOutputEnabled(enabled); + } + } + + public void setRemoteDebugging(final boolean enabled) { + if (mRuntime != null) { + mRuntime.getSettings().setRemoteDebuggingEnabled(enabled); + } + } + + public void setAutoplayEnabled(final boolean enabled) { + if (mRuntime != null) { + mRuntime.getSettings().setAutoplayDefault(enabled ? + GeckoRuntimeSettings.AUTOPLAY_DEFAULT_ALLOWED : + GeckoRuntimeSettings.AUTOPLAY_DEFAULT_BLOCKED); + } + } + + public boolean getAutoplayEnabled() { + if (mRuntime != null) { + return mRuntime.getSettings().getAutoplayDefault() == GeckoRuntimeSettings.AUTOPLAY_DEFAULT_ALLOWED ? + true : + false; + } + + return false; + } + + public void setLocales(List locales) { + if (mRuntime != null) { + mRuntime.getSettings().setLocales(locales.stream().toArray(String[]::new)); + } + } + + // Permission Delegate + + @Override + public void onAndroidPermissionsRequest(@NonNull GeckoSession session, @Nullable String[] permissions, @NonNull Callback callback) { + if (mPermissionDelegate != null) { + mPermissionDelegate.onAndroidPermissionsRequest(session, permissions, callback); + } + } + + @Override + public void onContentPermissionRequest(@NonNull GeckoSession session, @Nullable String uri, int type, @NonNull Callback callback) { + if (mPermissionDelegate != null) { + mPermissionDelegate.onContentPermissionRequest(session, uri, type, callback); + } + } + + @Override + public void onMediaPermissionRequest(@NonNull GeckoSession session, @NonNull String uri, @Nullable MediaSource[] video, @Nullable MediaSource[] audio, @NonNull MediaCallback callback) { + if (mPermissionDelegate != null) { + mPermissionDelegate.onMediaPermissionRequest(session, uri, video, audio, callback); + } + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionUtils.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionUtils.java new file mode 100644 index 000000000..cc40b50f4 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionUtils.java @@ -0,0 +1,114 @@ +package org.mozilla.vrbrowser.browser.engine; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.Nullable; + +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.vrbrowser.browser.SettingsStore; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +class SessionUtils { + + private static final String LOGTAG = SessionUtils.class.getCanonicalName(); + + public static boolean isLocalizedContent(@Nullable String url) { + return url != null && (url.startsWith("about:") || url.startsWith("data:")); + } + + public static void vrPrefsWorkAround(Context aContext, Bundle aExtras) { + File path = GeckoProfile.initFromArgs(aContext, null).getDir(); + String prefFileName = path.getAbsolutePath() + File.separator + "user.js"; + Log.i(LOGTAG, "Creating file: " + prefFileName); + try (FileOutputStream out = new FileOutputStream(prefFileName)) { + out.write("pref(\"dom.vr.enabled\", true);\n".getBytes()); + out.write("pref(\"dom.vr.external.enabled\", true);\n".getBytes()); + out.write("pref(\"webgl.enable-surface-texture\", true);\n".getBytes()); + // Enable MultiView draft extension + out.write("pref(\"webgl.enable-draft-extensions\", true);\n".getBytes()); + out.write("pref(\"apz.allow_double_tap_zooming\", false);\n".getBytes()); + out.write("pref(\"dom.webcomponents.customelements.enabled\", true);\n".getBytes()); + out.write("pref(\"javascript.options.ion\", true);\n".getBytes()); + out.write("pref(\"media.webspeech.synth.enabled\", false);\n".getBytes()); + // Prevent autozoom when giving a form field focus. + out.write("pref(\"formhelper.autozoom\", false);\n".getBytes()); + // Uncomment this to enable WebRender. WARNING NOT READY FOR USAGE. + // out.write("pref(\"gfx.webrender.all\", true);\n".getBytes()); + int msaa = SettingsStore.getInstance(aContext).getMSAALevel(); + if (msaa > 0) { + int msaaLevel = msaa == 2 ? 4 : 2; + out.write(("pref(\"gl.msaa-level\"," + msaaLevel + ");\n").getBytes()); + } + addOptionalPref(out, "dom.vr.require-gesture", aExtras); + addOptionalPref(out, "privacy.reduceTimerPrecision", aExtras); + if (aExtras != null && aExtras.getBoolean("media.autoplay.enabled", false)) { + // Enable playing audios without gesture (used for gfx automated testing) + out.write("pref(\"media.autoplay.enabled.user-gestures-needed\", false);\n".getBytes()); + out.write("pref(\"media.autoplay.enabled.ask-permission\", false);\n".getBytes()); + out.write("pref(\"media.autoplay.default\", 0);\n".getBytes()); + } + } catch (FileNotFoundException e) { + Log.e(LOGTAG, "Unable to create file: '" + prefFileName + "' got exception: " + e.toString()); + } catch (IOException e) { + Log.e(LOGTAG, "Unable to write file: '" + prefFileName + "' got exception: " + e.toString()); + } + } + + private static void addOptionalPref(FileOutputStream out, String aKey, Bundle aExtras) throws IOException { + if (aExtras != null && aExtras.containsKey(aKey)) { + boolean value = aExtras.getBoolean(aKey); + out.write(String.format("pref(\"%s\", %s);\n", aKey, value ? "true" : "false").getBytes()); + } + } + + /** + * 1. Disable YouTube's Polymer layout (which makes YouTube very slow in non-Chrome browsers) + * via a query-string parameter in the URL. + * 2. Rewrite YouTube URLs from `m.youtube.com` -> `youtube.com` (to avoid serving YouTube's + * video pages intended for mobile phones, as linked from Google search results). + */ + public static String checkYoutubeOverride(String aUri) { + try { + Uri uri = Uri.parse(aUri); + String hostLower = uri.getHost().toLowerCase(); + if (!hostLower.endsWith(".youtube.com") && !hostLower.endsWith(".youtube-nocookie.com")) { + return null; + } + + Uri.Builder uriBuilder = uri.buildUpon(); + Boolean updateUri = false; + + if (!uri.getScheme().equalsIgnoreCase("https")) { + uriBuilder.scheme("https"); + updateUri = true; + } + if (hostLower.startsWith("m.")) { + uriBuilder.authority(hostLower.replaceFirst("m.", "www.")); + updateUri = true; + } + String queryDisablePolymer = uri.getQueryParameter("disable_polymer"); + if (queryDisablePolymer == null) { + uriBuilder.appendQueryParameter("disable_polymer", "1"); + updateUri = true; + } + + if (!updateUri) { + return null; + } + + return uriBuilder.build().toString(); + } catch (Exception ex) { + Log.e(LOGTAG, "Unable to construct transformed URL: " + ex.toString()); + } + + return null; + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/search/suggestions/SuggestionsProvider.java b/app/src/common/shared/org/mozilla/vrbrowser/search/suggestions/SuggestionsProvider.java index 4267e5c90..a732c7e5c 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/search/suggestions/SuggestionsProvider.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/search/suggestions/SuggestionsProvider.java @@ -4,7 +4,7 @@ import androidx.annotation.NonNull; -import org.mozilla.vrbrowser.browser.SessionStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.search.SearchEngineWrapper; import org.mozilla.vrbrowser.ui.widgets.SuggestionsWidget.SuggestionItem; import org.mozilla.vrbrowser.ui.widgets.SuggestionsWidget.SuggestionItem.Type; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryHistogram.java b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryHistogram.java new file mode 100644 index 000000000..6398d8720 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryHistogram.java @@ -0,0 +1,43 @@ +package org.mozilla.vrbrowser.telemetry; + +import static java.lang.Math.toIntExact; + +public class TelemetryHistogram { + + private int[] mHistogram; + private int mNumBins; + private int mBinSize; + private long mMin; + + public TelemetryHistogram(int numBins, int binSize, long min) { + mNumBins = numBins; + mBinSize = binSize; + mMin = min; + mHistogram = new int[mNumBins]; + } + + public void addData(long data) { + if (data < mMin) { + return; + } + + int bin = toIntExact(data / mBinSize); + if (bin > (mNumBins - 2)) { + bin = mNumBins - 1; + + } else if (bin < 0) { + bin = 0; + } + + mHistogram[bin]++; + } + + public int[] getHistogram() { + return mHistogram; + } + + public int getBinSize() { + return mBinSize; + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java index 8a32ab778..66d07665b 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java @@ -27,16 +27,20 @@ import org.mozilla.vrbrowser.utils.UrlUtils; import java.net.URI; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; +import androidx.annotation.NonNull; import androidx.annotation.UiThread; import static java.lang.Math.toIntExact; +import static org.mozilla.vrbrowser.ui.widgets.Windows.*; public class TelemetryWrapper { private final static String APP_NAME = "FirefoxReality"; - private final static String LOGTAG = "VRB"; + private final static String LOGTAG = TelemetryWrapper.class.getSimpleName(); private final static int MIN_LOAD_TIME = 40; private final static int LOADING_BUCKET_SIZE_MS = 100; private final static int MIN_IMMERSIVE_TIME = 1000; @@ -50,6 +54,23 @@ public class TelemetryWrapper { private static int numUri = 0; private static long startLoadPageTime = 0; private static long startImmersiveTime = 0; + private static long sessionStartTime = 0; + + // Multi-window events + private final static int MULTI_WINDOW_BIN_SIZE_MS = 10000; + private static HashMap windowLifetime = new HashMap<>(); + private static int windowsMovesCount = 0; + private static int windowsResizesCount = 0; + private static long[] activePlacementStartTime = new long[MAX_WINDOWS]; + private static long[] activePlacementTime = new long[MAX_WINDOWS]; + private static long[] openWindowsStartTime = new long[MAX_WINDOWS]; + private static long[] openPrivateWindowsStartTime = new long[MAX_WINDOWS]; + private static long[] openWindowsTime = new long[MAX_WINDOWS]; + private static long[] openPrivateWindowsTime = new long[MAX_WINDOWS]; + private static int[] openWindows = new int[MAX_WINDOWS]; + private static int[] openPrivateWindows = new int[MAX_WINDOWS]; + private static TelemetryHistogram windowsLifetimeHistogram = + new TelemetryHistogram(HISTOGRAM_SIZE, MULTI_WINDOW_BIN_SIZE_MS, 0); private class Category { private static final String ACTION = "action"; @@ -65,6 +86,27 @@ private class Method { // TODO: Support "select_query" after providing search suggestion. private static final String VOICE_QUERY = "voice_query"; private static final String IMMERSIVE_MODE = "immersive_mode"; + + // How many max-windows dialogs happen / what percentage + private static final String MAX_WINDOWS_DIALOG = "max_windows_dialog"; + // New Window tray button use + private static final String NEW_WINDOW_BUTTON = "tray_new_window"; + // Long press to bring the context menu event + private static final String LONG_PRESS_CONTEXT_MENU = "context_menu"; + // How long is a window open for / window life + private static final String WINDOW_LIFETIME = "window_lifetime"; + // Frequency of window moves + private static final String WINDOWS_MOVES_FREQ = "windows_move_freq"; + // Frequency of window resizes + private static final String WINDOWS_RESIZE_FREQ = "windows_resize_freq"; + // When a session is multi-window, what percentage of time is each position the active window + private static final String PLACEMENTS_ACTIVE_TIME_PCT = "placements_active_time_pct"; + // When a session is multi-window, what percentage of time are one, two or three windows open + private static final String OPEN_WINDOWS_TIME_PCT = "open_windows_time_pct"; + // Curved windows setting - how many users use it / what percentage + private static final String CURVED_MODE_ACTIVE = "curved_mode_active"; + // Average of how many windows are open at a time, per session + private static final String WINDOWS_OPEN_W_MEAN = "windows_open_w_mean"; } private class Object { @@ -72,11 +114,23 @@ private class Object { private static final String BROWSER = "browser"; private static final String SEARCH_BAR = "search_bar"; private static final String VOICE_INPUT = "voice_input"; + private static final String WINDOW = "window"; + private static final String TRAY = "tray"; } private class Extra { private static final String TOTAL_URI_COUNT = "total_uri_count"; private static final String UNIQUE_DOMAINS_COUNT = "unique_domains_count"; + private static final String WINDOW_MOVES_FREQ = "windows_moves_freq"; + private static final String WINDOW_RESIZE_FREQ = "windows_resize_freq"; + private static final String LEFT_WINDOW_ACTIVE_TIME_PCT = "left_window_active_time_pct"; + private static final String FRONT_WINDOW_ACTIVE_TIME_PCT = "front_window_active_time_pct"; + private static final String RIGHT_WINDOW_ACTIVE_TIME_PCT = "right_window_active_time_pct"; + private static final String ONE_OPEN_WINDOWS_TIME_PCT = "one_windows_open_time_pct"; + private static final String TWO_OPEN_WINDOWS_TIME_PCT = "two_windows_open_time_pct"; + private static final String THREE_OPEN_WINDOWS_TIME_PCT = "three_windows_open_time_pct"; + private static final String WINDOWS_OPEN_W_MEAN = "window_open_w_mean"; + private static final String WINDOWS_PRIVATE_OPEN_W_MEAN = "window_open_private_w_mean"; } // We should call this at the application initial stage. Instead, @@ -97,7 +151,7 @@ public static void init(Context aContext) { .setCollectionEnabled(telemetryEnabled) .setUploadEnabled(telemetryEnabled) .setBuildId(String.valueOf(BuildConfig.VERSION_CODE)); - + final JSONPingSerializer serializer = new JSONPingSerializer(); final FileTelemetryStorage storage = new FileTelemetryStorage(configuration, serializer); TelemetryScheduler scheduler; @@ -114,11 +168,12 @@ public static void init(Context aContext) { } finally { StrictMode.setThreadPolicy(threadPolicy); } + + sessionStartTime = SystemClock.elapsedRealtime(); } @UiThread public static void start() { - queueHistogram(); // Call Telemetry.scheduleUpload() early. // See https://github.com/MozillaReality/FirefoxReality/issues/1353 TelemetryHolder.get() @@ -133,6 +188,7 @@ public static void start() { @UiThread public static void stop() { queueHistogram(); + queueMultiWindowEvents(); TelemetryEvent.create(Category.ACTION, Method.BACKGROUND, Object.APP).queue(); TelemetryHolder.get().recordSessionEnd(); @@ -292,5 +348,302 @@ public static void uploadImmersiveToHistogram() { immersiveHistogram[histogramImmersiveIndex]++; } -} + /** + * Helper method for queuing histograms. This will transform the raw histogram into + * a Telemetry historam event and queue it for future delivery. + * @param histogram The histogram to be queued + * @param method The TelemetryEvent method String + * @param object The TelemetryEvent object String + */ + private static void queueHistogram(@NonNull TelemetryHistogram histogram, @NonNull String method, @NonNull String object) { + TelemetryEvent event = TelemetryEvent.create(Category.HISTOGRAM, method, object); + int[] hist = histogram.getHistogram(); + for (int bucketIndex = 0; bucketIndex < hist.length; ++bucketIndex) { + event.extra( + Integer.toString(bucketIndex * histogram.getBinSize()), + Integer.toString(hist[bucketIndex])); + Log.d(LOGTAG, "\tHistogram bucket: [" + + "" + bucketIndex * histogram.getBinSize() + + ", " + hist[bucketIndex] + "]"); + } + event.queue(); + } + + // Multi-window related events + + public static void queueMultiWindowEvents() { + // Queue windows lifetime histogram + queueWindowsLifetimeHistogram(); + + // Queue Windows moves freq during the session + queueWindowsMovesCountEvent(); + + // Queue Windows resizes freq during the session + queueWindowsResizesCountEvent(); + + // Queue Windows active time pct + queueActiveWindowPctEvent(); + + // Queue Windows active time pct + queueOpenWindowsAvgEvent(); + + // Queue open Windows time pct + queueOpenWindowsPctEvent(); + } + + public static void trayNewWindowEvent() { + TelemetryEvent event = TelemetryEvent.create(Category.ACTION, Method.NEW_WINDOW_BUTTON, Object.TRAY); + event.queue(); + + Log.d(LOGTAG, "[Queue] Tray New Window Click"); + } + + public static void maxWindowsDialogEvent() { + TelemetryEvent event = TelemetryEvent.create(Category.ACTION, Method.MAX_WINDOWS_DIALOG, Object.WINDOW); + event.queue(); + + Log.d(LOGTAG, "[Queue] Max Windows dialog"); + } + + public static void longPressContextMenuEvent() { + TelemetryEvent event = TelemetryEvent.create(Category.ACTION, Method.LONG_PRESS_CONTEXT_MENU, Object.WINDOW); + event.queue(); + + Log.d(LOGTAG, "[Queue] Context Menu Long Press"); + } + + public static void openWindowEvent(int windowId) { + windowLifetime.put(windowId, SystemClock.elapsedRealtime()); + } + + public static void closeWindowEvent(int windowId) { + windowsLifetimeHistogram.addData(SystemClock.elapsedRealtime() - windowLifetime.get(windowId)); + windowLifetime.remove(windowId); + } + + public static void windowsMoveEvent() { + windowsMovesCount++; + + Log.d(LOGTAG, "Windows moves: " + windowsMovesCount); + } + + public static void windowsResizeEvent() { + windowsResizesCount++; + + Log.d(LOGTAG, "Windows resizes: " + windowsResizesCount); + } + + public static void activePlacementEvent(int from, boolean active) { + if (active) { + activePlacementStartTime[from] = SystemClock.elapsedRealtime(); + } else { + if (activePlacementStartTime[from] != 0) { + activePlacementTime[from] += SystemClock.elapsedRealtime() - activePlacementStartTime[from]; + activePlacementStartTime[from] = 0; + } + } + + Log.d(LOGTAG, "Placements times:"); + Log.d(LOGTAG, "\tFRONT: " + activePlacementTime[WindowPlacement.FRONT.getValue()]); + Log.d(LOGTAG, "\tLEFT: " + activePlacementTime[WindowPlacement.LEFT.getValue()]); + Log.d(LOGTAG, "\tRIGHT: " + activePlacementTime[WindowPlacement.RIGHT.getValue()]); + } + + public static void openWindowsEvent(int from, int to, boolean isPrivate) { + if (isPrivate) { + openPrivateWindows[to-1]++; + + if (from > 0) { + openPrivateWindowsTime[from-1] += SystemClock.elapsedRealtime() - openPrivateWindowsStartTime[from-1]; + openPrivateWindowsStartTime[from-1] = 0; + } + openPrivateWindowsStartTime[to-1] = SystemClock.elapsedRealtime(); + + Log.d(LOGTAG, "Placements times (private):"); + Log.d(LOGTAG, "\tONE: " + openPrivateWindowsTime[WindowPlacement.FRONT.getValue()]); + Log.d(LOGTAG, "\tTWO: " + openPrivateWindowsTime[WindowPlacement.LEFT.getValue()]); + Log.d(LOGTAG, "\tTHREE: " + openPrivateWindowsTime[WindowPlacement.RIGHT.getValue()]); + + Log.d(LOGTAG, "Open Windows Count (private):"); + Log.d(LOGTAG, "\tFRONT: " + openPrivateWindows[WindowPlacement.FRONT.getValue()]); + Log.d(LOGTAG, "\tLEFT: " + openPrivateWindows[WindowPlacement.LEFT.getValue()]); + Log.d(LOGTAG, "\tRIGHT: " + openPrivateWindows[WindowPlacement.RIGHT.getValue()]); + + } else { + openWindows[to-1]++; + + if (from > 0) { + openWindowsTime[from-1] += SystemClock.elapsedRealtime() - openWindowsStartTime[from-1]; + openWindowsStartTime[from-1] = 0; + } + openWindowsStartTime[to-1] = SystemClock.elapsedRealtime(); + + Log.d(LOGTAG, "Placements times:"); + Log.d(LOGTAG, "\tONE: " + openWindowsTime[WindowPlacement.FRONT.getValue()]); + Log.d(LOGTAG, "\tTWO: " + openWindowsTime[WindowPlacement.LEFT.getValue()]); + Log.d(LOGTAG, "\tTHREE: " + openWindowsTime[WindowPlacement.RIGHT.getValue()]); + + Log.d(LOGTAG, "Open Private Windows Count:"); + Log.d(LOGTAG, "\tFRONT: " + openWindows[WindowPlacement.FRONT.getValue()]); + Log.d(LOGTAG, "\tLEFT: " + openWindows[WindowPlacement.LEFT.getValue()]); + Log.d(LOGTAG, "\tRIGHT: " + openWindows[WindowPlacement.RIGHT.getValue()]); + } + } + + public static void queueCurvedModeActiveEvent() { + TelemetryEvent event = TelemetryEvent.create(Category.ACTION, Method.CURVED_MODE_ACTIVE, Object.WINDOW); + event.queue(); + + Log.d(LOGTAG, "[Queue] Curved mode active"); + } + + private static void queueWindowsLifetimeHistogram() { + for (Map.Entry entry : windowLifetime.entrySet()) { + windowsLifetimeHistogram.addData(SystemClock.elapsedRealtime() - entry.getValue()); + } + + Log.d(LOGTAG, "[Queue] Windows Lifetime Histogram:"); + queueHistogram(windowsLifetimeHistogram, Method.WINDOW_LIFETIME, Object.WINDOW); + windowsLifetimeHistogram = new TelemetryHistogram(HISTOGRAM_SIZE, MULTI_WINDOW_BIN_SIZE_MS, 0); + + for(Map.Entry entry : windowLifetime.entrySet()) { + windowLifetime.put(entry.getKey(), SystemClock.elapsedRealtime()); + } + } + + private static void queueWindowsMovesCountEvent() { + float movesFreqPerSession = (float)windowsMovesCount / ((SystemClock.elapsedRealtime() - sessionStartTime)/1000); + TelemetryEvent event = TelemetryEvent.create(Category.ACTION, Method.WINDOWS_MOVES_FREQ, Object.WINDOW); + event.extra(Extra.WINDOW_MOVES_FREQ, String.valueOf(movesFreqPerSession)); + event.queue(); + + Log.d(LOGTAG, "[Queue] Windows Moves Freq: " + movesFreqPerSession); + + windowsMovesCount = 0; + } + + private static void queueWindowsResizesCountEvent() { + float resizesFreqPerSession = (float)windowsResizesCount / ((SystemClock.elapsedRealtime() - sessionStartTime)/1000); + TelemetryEvent event = TelemetryEvent.create(Category.ACTION, Method.WINDOWS_RESIZE_FREQ, Object.WINDOW); + event.extra(Extra.WINDOW_RESIZE_FREQ, String.valueOf(resizesFreqPerSession)); + event.queue(); + + Log.d(LOGTAG, "[Queue] Windows Resizes Freq: " + resizesFreqPerSession); + + windowsResizesCount = 0; + } + + private static void queueActiveWindowPctEvent() { + for (int index = 0; index< MAX_WINDOWS; index++) { + if (activePlacementStartTime[index] != 0) { + activePlacementTime[index] += SystemClock.elapsedRealtime() - activePlacementStartTime[index]; + activePlacementStartTime[index] = SystemClock.elapsedRealtime(); + } + } + + long totalTime = 0; + for (long time : activePlacementTime) + totalTime += time; + + if (totalTime == 0) + return; + + float[] pcts = new float[MAX_WINDOWS]; + for (int index = 0; index< MAX_WINDOWS; index++) + pcts[index] = (activePlacementTime[index]*100.0f)/totalTime; + + TelemetryEvent event = TelemetryEvent.create(Category.ACTION, Method.PLACEMENTS_ACTIVE_TIME_PCT, Object.WINDOW); + event.extra(Extra.LEFT_WINDOW_ACTIVE_TIME_PCT, String.valueOf(pcts[WindowPlacement.LEFT.getValue()])); + event.extra(Extra.FRONT_WINDOW_ACTIVE_TIME_PCT, String.valueOf(pcts[WindowPlacement.FRONT.getValue()])); + event.extra(Extra.RIGHT_WINDOW_ACTIVE_TIME_PCT, String.valueOf(pcts[WindowPlacement.RIGHT.getValue()])); + event.queue(); + + Log.d(LOGTAG, "[Queue] Placements Active time Pct:"); + Log.d(LOGTAG, "\tFRONT: " + pcts[WindowPlacement.FRONT.getValue()]); + Log.d(LOGTAG, "\tLEFT: " + pcts[WindowPlacement.LEFT.getValue()]); + Log.d(LOGTAG, "\tRIGHT: " + pcts[WindowPlacement.RIGHT.getValue()]); + + for (int index = 0; index< MAX_WINDOWS; index++) { + activePlacementTime[index] = 0; + } + } + + private static void queueOpenWindowsAvgEvent() { + float weight = 0; + float weightSum = 0; + for(int i=0; i < MAX_WINDOWS; i++){ + weight += openWindows[i]; + weightSum += (i+1) * openWindows[i]; + } + float regularWM = weightSum > 0 ? weightSum/weight : 0; + + weight = 0; + weightSum = 0; + for(int i=0; i < MAX_WINDOWS; i++){ + weight += openPrivateWindows[i]; + weightSum += (i+1) * openPrivateWindows[i]; + } + float privateWM = weightSum > 0 ? weightSum/weight : 0; + + TelemetryEvent event = TelemetryEvent.create(Category.ACTION, Method.WINDOWS_OPEN_W_MEAN, Object.WINDOW); + event.extra(Extra.WINDOWS_OPEN_W_MEAN, String.valueOf(regularWM)); + event.extra(Extra.WINDOWS_PRIVATE_OPEN_W_MEAN, String.valueOf(privateWM)); + event.queue(); + + Log.d(LOGTAG, "[Queue] Open Windows Number Weighted Mean:"); + Log.d(LOGTAG, "\tRegular: " + regularWM); + Log.d(LOGTAG, "\tPrivate: " + privateWM); + + for (int index = 0; index< MAX_WINDOWS; index++) { + if (openWindows[index] != 0) + openWindows[index] = 1; + if (openPrivateWindows[index] != 0) + openPrivateWindows[index] = 1; + } + } + + private static void queueOpenWindowsPctEvent() { + for (int index = 0; index< MAX_WINDOWS; index++) { + if (openWindowsStartTime[index] != 0) { + openWindowsTime[index] += SystemClock.elapsedRealtime() - openWindowsStartTime[index]; + openWindowsStartTime[index] = SystemClock.elapsedRealtime(); + } + + if (openPrivateWindowsStartTime[index] != 0) { + openPrivateWindowsTime[index] += SystemClock.elapsedRealtime() - openPrivateWindowsStartTime[index]; + openPrivateWindowsStartTime[index] = SystemClock.elapsedRealtime(); + } + } + + long totalTime = 0; + for (long time : openWindowsTime) + totalTime += time; + for (long time : openPrivateWindowsTime) + totalTime += time; + + if (totalTime == 0) + return; + + float[] pcts = new float[MAX_WINDOWS]; + for (int index = 0; index< MAX_WINDOWS; index++) + pcts[index] = ((openWindowsTime[index]+openPrivateWindowsTime[index])*100.0f)/totalTime; + + TelemetryEvent event = TelemetryEvent.create(Category.ACTION, Method.OPEN_WINDOWS_TIME_PCT, Object.WINDOW); + event.extra(Extra.ONE_OPEN_WINDOWS_TIME_PCT, String.valueOf(pcts[WindowPlacement.LEFT.getValue()])); + event.extra(Extra.TWO_OPEN_WINDOWS_TIME_PCT, String.valueOf(pcts[WindowPlacement.FRONT.getValue()])); + event.extra(Extra.THREE_OPEN_WINDOWS_TIME_PCT, String.valueOf(pcts[WindowPlacement.RIGHT.getValue()])); + event.queue(); + + Log.d(LOGTAG, "[Queue] Open Windows time Pct:"); + Log.d(LOGTAG, "\tONE: " + pcts[WindowPlacement.FRONT.getValue()]); + Log.d(LOGTAG, "\tTWO: " + pcts[WindowPlacement.LEFT.getValue()]); + Log.d(LOGTAG, "\tTHREE: " + pcts[WindowPlacement.RIGHT.getValue()]); + + for (int index = 0; index< MAX_WINDOWS; index++) { + openWindowsTime[index] = 0; + openPrivateWindowsTime[index] = 0; + } + } + +} \ No newline at end of file diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BindingAdapters.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BindingAdapters.java index c2b27870b..1dbca1e38 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BindingAdapters.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BindingAdapters.java @@ -4,13 +4,14 @@ import android.view.View; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.databinding.BindingAdapter; public class BindingAdapters { @BindingAdapter("visibleGone") - public static void showHide(View view, boolean show) { + public static void showHide(@NonNull View view, boolean show) { view.setVisibility(show ? View.VISIBLE : View.GONE); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/ContextMenuAdapter.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/ContextMenuAdapter.java new file mode 100644 index 000000000..8e491e8e1 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/ContextMenuAdapter.java @@ -0,0 +1,107 @@ +package org.mozilla.vrbrowser.ui.adapters; + +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.databinding.DataBindingUtil; +import androidx.recyclerview.widget.RecyclerView; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.databinding.ContextMenuItemBinding; +import org.mozilla.vrbrowser.ui.callbacks.ContextMenuClickCallback; + +import java.util.List; + +public class ContextMenuAdapter extends RecyclerView.Adapter { + + private List mContextMenuList; + + public static class ContextMenuNode { + int position; + Drawable icon; + String title; + + public ContextMenuNode(int position, Drawable icon, String title) { + this.position = position; + this.icon = icon; + this.title = title; + } + + public Integer getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public Drawable getIcon() { + return icon; + } + + public void setIcon(Drawable icon) { + this.icon = icon; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + @Nullable + private final ContextMenuClickCallback mContextMenuItemClickCallback; + + public ContextMenuAdapter(@Nullable ContextMenuClickCallback clickCallback) { + mContextMenuItemClickCallback = clickCallback; + + setHasStableIds(true); + } + + public void setContextMenuItemList(final List contextMenuList) { + mContextMenuList = contextMenuList; + } + + @Override + public ContextMenuViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ContextMenuItemBinding binding = DataBindingUtil + .inflate(LayoutInflater.from(parent.getContext()), R.layout.context_menu_item, + parent, false); + binding.setCallback(mContextMenuItemClickCallback); + return new ContextMenuViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull ContextMenuViewHolder holder, int position) { + holder.binding.setMenuItem(mContextMenuList.get(position)); + holder.binding.executePendingBindings(); + } + + @Override + public int getItemCount() { + return mContextMenuList == null ? 0 : mContextMenuList.size(); + } + + @Override + public long getItemId(int position) { + ContextMenuNode menuItem = mContextMenuList.get(position); + return menuItem.getPosition() != null ? menuItem.getPosition() : RecyclerView.NO_ID; + } + + static class ContextMenuViewHolder extends RecyclerView.ViewHolder { + + final ContextMenuItemBinding binding; + + ContextMenuViewHolder(@NonNull ContextMenuItemBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/ContextMenuClickCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/ContextMenuClickCallback.java new file mode 100644 index 000000000..58cc7e8c2 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/ContextMenuClickCallback.java @@ -0,0 +1,7 @@ +package org.mozilla.vrbrowser.ui.callbacks; + +import org.mozilla.vrbrowser.ui.adapters.ContextMenuAdapter; + +public interface ContextMenuClickCallback { + void onClick(ContextMenuAdapter.ContextMenuNode contextMenuNode); +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/NavigationBarCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/NavigationBarCallback.java deleted file mode 100644 index 790c37c71..000000000 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/NavigationBarCallback.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.mozilla.vrbrowser.ui.callbacks; - -import org.mozilla.vrbrowser.model.Bookmark; - -public interface NavigationBarCallback { - void onBookmarkClick(Bookmark bookmark); -} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/BookmarksView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/BookmarksView.java index 0e02ebdc2..f5ad8e142 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/BookmarksView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/BookmarksView.java @@ -8,41 +8,29 @@ import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; -import android.view.View; import android.widget.FrameLayout; -import org.mozilla.geckoview.AllowOrDeny; -import org.mozilla.geckoview.GeckoResult; -import org.mozilla.geckoview.GeckoSession; -import org.mozilla.geckoview.WebRequestError; +import androidx.databinding.DataBindingUtil; + import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; import org.mozilla.vrbrowser.browser.BookmarksStore; -import org.mozilla.vrbrowser.browser.SessionStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; +import org.mozilla.vrbrowser.browser.engine.SessionStack; import org.mozilla.vrbrowser.databinding.BookmarksBinding; import org.mozilla.vrbrowser.ui.adapters.BookmarkAdapter; import org.mozilla.vrbrowser.ui.callbacks.BookmarkClickCallback; -import org.mozilla.vrbrowser.ui.widgets.BookmarkListener; import org.mozilla.vrbrowser.utils.UIThreadExecutor; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.databinding.DataBindingUtil; - import mozilla.components.concept.storage.BookmarkNode; import mozilla.components.concept.storage.VisitType; -public class BookmarksView extends FrameLayout implements GeckoSession.NavigationDelegate, BookmarksStore.BookmarkListener { - - private static final String ABOUT_BLANK = "about:blank"; +public class BookmarksView extends FrameLayout implements BookmarksStore.BookmarkListener { private BookmarksBinding mBinding; private BookmarkAdapter mBookmarkAdapter; - private List mBookmarkListeners; private AudioEngine mAudio; private boolean mIgnoreNextListener; @@ -62,10 +50,8 @@ public BookmarksView(Context aContext, AttributeSet aAttrs, int aDefStyle) { } private void initialize(Context aContext) { - mBookmarkListeners = new ArrayList<>(); - mAudio = AudioEngine.fromContext(aContext); - SessionStore.get().addNavigationListener(this); + LayoutInflater inflater = LayoutInflater.from(aContext); // Inflate this data binding layout @@ -80,23 +66,10 @@ private void initialize(Context aContext) { setVisibility(GONE); } - public void addListeners(BookmarkListener... listeners) { - mBookmarkListeners.addAll(Arrays.asList(listeners)); - } - public void onDestroy() { - mBookmarkListeners.clear(); SessionStore.get().getBookmarkStore().removeListener(this); } - private void notifyBookmarksShown() { - mBookmarkListeners.forEach(BookmarkListener::onBookmarksShown); - } - - private void notifyBookmarksHidden() { - mBookmarkListeners.forEach(BookmarkListener::onBookmarksHidden); - } - private final BookmarkClickCallback mBookmarkClickCallback = new BookmarkClickCallback() { @Override public void onClick(BookmarkNode bookmark) { @@ -105,7 +78,9 @@ public void onClick(BookmarkNode bookmark) { } SessionStore.get().getHistoryStore().addHistory(bookmark.getUrl(), VisitType.BOOKMARK); - SessionStore.get().loadUri(bookmark.getUrl()); + + SessionStack sessionStack = SessionStore.get().getActiveStore(); + sessionStack.loadUri(bookmark.getUrl()); } @Override @@ -143,56 +118,6 @@ private void showBookmarks(List aBookmarks) { mBinding.executePendingBindings(); } - @Override - public void setVisibility(int visibility) { - super.setVisibility(visibility); - - if (visibility == VISIBLE) { - notifyBookmarksShown(); - - } else { - notifyBookmarksHidden(); - } - } - - // NavigationDelegate - - @Override - public void onLocationChange(GeckoSession session, String url) { - if (getVisibility() == View.VISIBLE && - url != null && - !url.equals(ABOUT_BLANK)) { - notifyBookmarksHidden(); - } - } - - @Override - public void onCanGoBack(GeckoSession session, boolean canGoBack) { - - } - - @Override - public void onCanGoForward(GeckoSession session, boolean canGoForward) { - - } - - @Nullable - @Override - public GeckoResult onLoadRequest(@NonNull GeckoSession session, @NonNull LoadRequest request) { - return GeckoResult.ALLOW; - } - - @Nullable - @Override - public GeckoResult onNewSession(@NonNull GeckoSession session, @NonNull String uri) { - return null; - } - - @Override - public GeckoResult onLoadError(GeckoSession session, String uri, WebRequestError error) { - return null; - } - // BookmarksStore.BookmarkListener @Override public void onBookmarksUpdated() { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/ContextMenu.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/ContextMenu.java new file mode 100644 index 000000000..7f3d352a7 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/ContextMenu.java @@ -0,0 +1,96 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.vrbrowser.ui.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.FrameLayout; + +import androidx.databinding.DataBindingUtil; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.databinding.ContextMenuBinding; +import org.mozilla.vrbrowser.ui.adapters.ContextMenuAdapter; +import org.mozilla.vrbrowser.ui.callbacks.ContextMenuClickCallback; + +import java.util.Arrays; +import java.util.List; + +public class ContextMenu extends FrameLayout { + + private ContextMenuBinding mBinding; + private ContextMenuAdapter mContextMenuAdapter; + private ContextMenuClickCallback mCallback; + + public ContextMenu(Context aContext) { + super(aContext); + initialize(aContext); + } + + public ContextMenu(Context aContext, AttributeSet aAttrs) { + super(aContext, aAttrs); + initialize(aContext); + } + + public ContextMenu(Context aContext, AttributeSet aAttrs, int aDefStyle) { + super(aContext, aAttrs, aDefStyle); + initialize(aContext); + } + + private void initialize(Context aContext) { + LayoutInflater inflater = LayoutInflater.from(aContext); + + mBinding = DataBindingUtil.inflate(inflater, R.layout.context_menu, this, true); + mContextMenuAdapter = new ContextMenuAdapter(mContextMenuClickCallback); + mBinding.contextMenuList.setAdapter(mContextMenuAdapter); + mBinding.executePendingBindings(); + } + + public void setContextMenuClickCallback(ContextMenuClickCallback callback) { + mCallback = callback; + } + + private final ContextMenuClickCallback mContextMenuClickCallback = contextMenuNode -> { + if (mCallback != null) { + mCallback.onClick(contextMenuNode); + } + }; + + public void createLinkContextMenu() { + List contextMenuItems = buildBaseContextMenu(); + mContextMenuAdapter.setContextMenuItemList(contextMenuItems); + mBinding.executePendingBindings(); + } + + public void createAudioContextMenu() { + List contextMenuItems = buildBaseContextMenu(); + mContextMenuAdapter.setContextMenuItemList(contextMenuItems); + mBinding.executePendingBindings(); + } + + public void createVideoContextMenu() { + List contextMenuItems = buildBaseContextMenu(); + mContextMenuAdapter.setContextMenuItemList(contextMenuItems); + mBinding.executePendingBindings(); + } + + public void createImageContextMenu() { + List contextMenuItems = buildBaseContextMenu(); + mContextMenuAdapter.setContextMenuItemList(contextMenuItems); + mBinding.executePendingBindings(); + } + + private List buildBaseContextMenu() { + return Arrays.asList( + new ContextMenuAdapter.ContextMenuNode( + 0, + getResources().getDrawable(R.drawable.ic_context_menu_new_window, getContext().getTheme()), + "Open in a New Window") + ); + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/NavigationURLBar.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/NavigationURLBar.java index 212af0ba9..1a644ae72 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/NavigationURLBar.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/NavigationURLBar.java @@ -16,6 +16,7 @@ import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; @@ -30,8 +31,8 @@ import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; import org.mozilla.vrbrowser.browser.BookmarksStore; -import org.mozilla.vrbrowser.browser.SessionStore; -import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; +import org.mozilla.vrbrowser.browser.engine.SessionStack; import org.mozilla.vrbrowser.search.SearchEngineWrapper; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; @@ -71,6 +72,7 @@ public class NavigationURLBar extends FrameLayout { private boolean mBookmarkEnabled = true; private boolean mIsContextButtonsEnabled = true; private UIThreadExecutor mUIThreadExecutor = new UIThreadExecutor(); + private SessionStack mSessionStack; private Unit domainAutocompleteFilter(String text) { if (mURL != null) { @@ -102,6 +104,8 @@ public NavigationURLBar(Context context, AttributeSet attrs) { private void initialize(Context aContext) { mAudio = AudioEngine.fromContext(aContext); + mSessionStack = SessionStore.get().getActiveStore(); + // Inflate this data binding layout inflate(aContext, R.layout.navigation_url, this); @@ -150,7 +154,7 @@ private void initialize(Context aContext) { mUAModeButton = findViewById(R.id.uaModeButton); mUAModeButton.setTag(R.string.view_id_tag, R.id.uaModeButton); mUAModeButton.setOnClickListener(mUAModeListener); - setUAMode(SettingsStore.getInstance(aContext).getUaMode()); + setUAMode(mSessionStack.getUaMode()); mURLLeftContainer = findViewById(R.id.urlLeftContainer); mInsecureIcon = findViewById(R.id.insecureIcon); @@ -168,8 +172,6 @@ private void initialize(Context aContext) { // Bookmarks mBookmarkButton = findViewById(R.id.bookmarkButton); mBookmarkButton.setOnClickListener(v -> handleBookmarkClick()); - - setURL(""); mIsBookmarkMode = false; // Prevent the URL TextEdit to get focus when user touches something outside of it @@ -178,6 +180,11 @@ private void initialize(Context aContext) { syncViews(); } + public void setSessionStack(SessionStack sessionStack) { + mSessionStack = sessionStack; + setUAMode(mSessionStack.getUaMode()); + } + public void onPause() { if (mIsLoading) { mLoadingView.clearAnimation(); @@ -214,19 +221,34 @@ public void setIsBookmarkMode(boolean isBookmarkMode) { syncViews(); } + public boolean isInBookmarkMode() { + return mIsBookmarkMode; + } + + private void setBookmarkEnabled(boolean aEnabled) { + if (mBookmarkEnabled != aEnabled) { + mBookmarkEnabled = aEnabled; + mBookmarkButton.setVisibility(aEnabled ? View.VISIBLE : View.GONE); + ViewGroup.LayoutParams params = mMicrophoneButton.getLayoutParams(); + params.width = (int) getResources().getDimension(aEnabled ? R.dimen.url_bar_item_width : R.dimen.url_bar_last_item_width); + mMicrophoneButton.setLayoutParams(params); + mMicrophoneButton.setBackgroundResource(aEnabled ? R.drawable.url_button : R.drawable.url_button_end); + } + } + private void handleBookmarkClick() { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } - String url = SessionStore.get().getCurrentUri(); + String url = mSessionStack.getCurrentUri(); if (StringUtils.isEmpty(url)) { return; } BookmarksStore bookmarkStore = SessionStore.get().getBookmarkStore(); bookmarkStore.isBookmarked(url).thenAcceptAsync(bookmarked -> { if (!bookmarked) { - bookmarkStore.addBookmark(url, SessionStore.get().getCurrentTitle()); + bookmarkStore.addBookmark(url, mSessionStack.getCurrentTitle()); setBookmarked(true); } else { // Delete @@ -258,7 +280,7 @@ public void setURL(String aURL) { if (StringUtils.isEmpty(aURL)) { setBookmarked(false); } else { - SessionStore.get().getBookmarkStore().isBookmarked(aURL).thenAcceptAsync(this::setBookmarked, mUIThreadExecutor); + SessionStore.get().getBookmarkStore().isBookmarked(aURL).thenAcceptAsync(this::setBookmarked, mUIThreadExecutor); } int index = -1; @@ -271,9 +293,9 @@ public void setURL(String aURL) { } if (aURL.startsWith("jar:")) return; - else if (aURL.startsWith("resource:") || SessionStore.get().isHomeUri(aURL)) + else if (aURL.startsWith("resource:") || mSessionStack.isHomeUri(aURL)) aURL = ""; - else if (aURL.startsWith("data:") && SessionStore.get().isCurrentSessionPrivate()) + else if (aURL.startsWith("data:") && mSessionStack.isPrivateMode()) aURL = ""; else index = aURL.indexOf("://"); @@ -434,9 +456,9 @@ public void handleURLEdit(String text) { TelemetryWrapper.urlBarEvent(false); } - if (SessionStore.get().getCurrentUri() != url) { + if (mSessionStack.getCurrentUri() != url) { SessionStore.get().getHistoryStore().addHistory(url, VisitType.TYPED); - SessionStore.get().loadUri(url); + mSessionStack.loadUri(url); if (mDelegate != null) { mDelegate.onHideSearchPopup(); @@ -478,14 +500,14 @@ public void setClickable(boolean clickable) { view.requestFocusFromTouch(); - int uaMode = SessionStore.get().getUaMode(); - if (uaMode == GeckoSessionSettings.USER_AGENT_MODE_VR) { - setUAMode(GeckoSessionSettings.USER_AGENT_MODE_DESKTOP); - SessionStore.get().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_DESKTOP); + int uaMode = mSessionStack.getUaMode(); + if (uaMode == GeckoSessionSettings.USER_AGENT_MODE_DESKTOP) { + setUAMode(GeckoSessionSettings.USER_AGENT_MODE_VR); + mSessionStack.setUaMode(GeckoSessionSettings.USER_AGENT_MODE_VR); }else { - setUAMode(GeckoSessionSettings.USER_AGENT_MODE_VR); - SessionStore.get().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_VR); + setUAMode(GeckoSessionSettings.USER_AGENT_MODE_DESKTOP); + mSessionStack.setUaMode(GeckoSessionSettings.USER_AGENT_MODE_DESKTOP); } TelemetryWrapper.voiceInputEvent(); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BookmarkListener.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BookmarkListener.java index 0a47dd1bc..534bc6467 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BookmarkListener.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BookmarkListener.java @@ -1,6 +1,6 @@ package org.mozilla.vrbrowser.ui.widgets; public interface BookmarkListener { - void onBookmarksShown(); - void onBookmarksHidden(); + default void onBookmarksShown(WindowWidget aWindow) {}; + default void onBookmarksHidden(WindowWidget aWindow) {}; } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrightnessMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrightnessMenuWidget.java index 4e97dd940..57dbf42ed 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrightnessMenuWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrightnessMenuWidget.java @@ -23,7 +23,7 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.anchorX = 0.0f; aPlacement.anchorY = 0.0f; aPlacement.translationY = WidgetPlacement.dpDimension(getContext(), R.dimen.video_projection_menu_translation_y); - aPlacement.translationZ = 1.0f; + aPlacement.translationZ = 30.0f; } public void setParentWidget(UIWidget aParent) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/KeyboardWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/KeyboardWidget.java index 4b7753518..e3063501e 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/KeyboardWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/KeyboardWidget.java @@ -18,9 +18,7 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.widget.ImageButton; @@ -31,7 +29,7 @@ import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.browser.SessionStore; +import org.mozilla.vrbrowser.browser.engine.SessionStack; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.input.CustomKeyboard; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; @@ -79,7 +77,7 @@ public class KeyboardWidget extends UIWidget implements CustomKeyboardView.OnKey private View mFocusedView; private LinearLayout mKeyboardLayout; private RelativeLayout mKeyboardContainer; - private UIWidget mBrowserWidget; + private UIWidget mAttachedWindow; private InputConnection mInputConnection; private EditorInfo mEditorInfo = new EditorInfo(); private VoiceSearchWidget mVoiceSearchWidget; @@ -98,6 +96,7 @@ public class KeyboardWidget extends UIWidget implements CustomKeyboardView.OnKey private String mComposingText = ""; private String mComposingDisplayText = ""; private boolean mInternalDeleteHint = false; + private SessionStack mSessionStack; private class MoveTouchListener implements OnTouchListener { @Override @@ -237,16 +236,15 @@ private void initialize(Context aContext) { mAutoCompletionView.setExtendedHeight((int)(mWidgetPlacement.height * mWidgetPlacement.density)); mAutoCompletionView.setDelegate(this); - SessionStore.get().addTextInputListener(this); updateCandidates(); } @Override public void releaseWidget() { + detachFromWindow(); mWidgetManager.removeFocusChangeListener(this); - SessionStore.get().removeTextInputListener(this); mAutoCompletionView.setDelegate(null); - mBrowserWidget = null; + mAttachedWindow = null; super.releaseWidget(); } @@ -259,9 +257,10 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.height += WidgetPlacement.dpDimension(context, R.dimen.keyboard_layout_padding); aPlacement.anchorX = 0.5f; aPlacement.anchorY = 0.0f; + aPlacement.parentAnchorY = 0.0f; aPlacement.translationX = WidgetPlacement.unitFromMeters(context, R.dimen.keyboard_x); - aPlacement.translationY = WidgetPlacement.unitFromMeters(context, R.dimen.keyboard_y); - aPlacement.translationZ = WidgetPlacement.unitFromMeters(context, R.dimen.keyboard_z); + aPlacement.translationY = WidgetPlacement.unitFromMeters(context, R.dimen.keyboard_y) - WidgetPlacement.unitFromMeters(context, R.dimen.window_world_y); + aPlacement.translationZ = WidgetPlacement.unitFromMeters(context, R.dimen.keyboard_z) - WidgetPlacement.unitFromMeters(context, R.dimen.window_world_z); aPlacement.rotationAxisX = 1.0f; aPlacement.rotation = (float)Math.toRadians(WidgetPlacement.floatDimension(context, R.dimen.keyboard_world_rotation)); aPlacement.worldWidth = WidgetPlacement.floatDimension(context, R.dimen.keyboard_world_width); @@ -269,6 +268,28 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.cylinder = true; } + @Override + public void detachFromWindow() { + if (mSessionStack != null) { + mSessionStack.removeTextInputListener(this); + mSessionStack = null; + } + } + + @Override + public void attachToWindow(@NonNull WindowWidget aWindow) { + if (mAttachedWindow == aWindow) { + return; + } + mAttachedWindow = aWindow; + mWidgetPlacement.parentHandle = aWindow.getHandle(); + + mSessionStack = aWindow.getSessionStack(); + if (mSessionStack != null) { + mSessionStack.addTextInputListener(this); + } + } + private int getKeyboardWidth(float aAlphabeticWidth) { float width = aAlphabeticWidth; width += WidgetPlacement.dpDimension(getContext(), R.dimen.keyboard_layout_padding) * 2; @@ -277,10 +298,6 @@ private int getKeyboardWidth(float aAlphabeticWidth) { return (int) width; } - public void setBrowserWidget(UIWidget aWidget) { - mBrowserWidget = aWidget; - } - private void resetKeyboardLayout() { if ((mEditorInfo.inputType & EditorInfo.TYPE_CLASS_NUMBER) == EditorInfo.TYPE_CLASS_NUMBER) { mKeyboardView.setKeyboard(getSymbolsKeyboard()); @@ -328,7 +345,7 @@ public void updateFocusedView(View aFocusedView) { public void dismiss() { exitVoiceInputMode(); - if (mFocusedView != null && mFocusedView != mBrowserWidget) { + if (mFocusedView != null && mFocusedView != mAttachedWindow) { mFocusedView.clearFocus(); } mWidgetPlacement.visible = false; @@ -874,21 +891,21 @@ public void restartInput(@NonNull GeckoSession session, int reason) { @Override public void showSoftInput(@NonNull GeckoSession session) { - if (mFocusedView != mBrowserWidget || getVisibility() != View.VISIBLE) { - updateFocusedView(mBrowserWidget); + if (mFocusedView != mAttachedWindow || getVisibility() != View.VISIBLE) { + updateFocusedView(mAttachedWindow); } } @Override public void hideSoftInput(@NonNull GeckoSession session) { - if (mFocusedView == mBrowserWidget && getVisibility() == View.VISIBLE) { + if (mFocusedView == mAttachedWindow && getVisibility() == View.VISIBLE) { dismiss(); } } @Override public void updateSelection(@NonNull GeckoSession session, final int selStart, final int selEnd, final int compositionStart, final int compositionEnd) { - if (mFocusedView != mBrowserWidget || mInputConnection == null) { + if (mFocusedView != mAttachedWindow || mInputConnection == null) { return; } @@ -904,21 +921,6 @@ public void run() { }); } - @Override - public void updateExtractedText(@NonNull GeckoSession session, @NonNull ExtractedTextRequest request, @NonNull ExtractedText text) { - - } - - @Override - public void updateCursorAnchorInfo(@NonNull GeckoSession session, @NonNull CursorAnchorInfo info) { - - } - - @Override - public void notifyAutoFill(GeckoSession session, int notification, int virtualId) { - - } - // FocusChangeListener @Override diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MediaControlsWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MediaControlsWidget.java index 91088b0fd..5e2fcde10 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MediaControlsWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MediaControlsWidget.java @@ -219,6 +219,7 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.anchorY = 0.5f; aPlacement.parentAnchorX = 0.5f; aPlacement.parentAnchorY = 0.5f; + aPlacement.cylinderMapRadius = 0.0f; // Do not map X when this widget uses cylindrical layout. } public void setParentWidget(int aHandle) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java index 10b87075b..889cac8e9 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java @@ -12,6 +12,7 @@ import android.preference.PreferenceManager; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; import android.view.View; import android.view.ViewGroup; @@ -21,13 +22,15 @@ import org.mozilla.geckoview.AllowOrDeny; import org.mozilla.geckoview.GeckoResult; import org.mozilla.geckoview.GeckoSession; -import org.mozilla.geckoview.WebRequestError; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; import org.mozilla.vrbrowser.browser.Media; -import org.mozilla.vrbrowser.browser.SessionStore; +import org.mozilla.vrbrowser.browser.SessionChangeListener; +import org.mozilla.vrbrowser.browser.engine.SessionStore; +import org.mozilla.vrbrowser.browser.engine.SessionStack; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.search.suggestions.SuggestionsProvider; +import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.views.CustomUIButton; import org.mozilla.vrbrowser.ui.views.NavigationURLBar; import org.mozilla.vrbrowser.ui.views.UIButton; @@ -45,7 +48,7 @@ public class NavigationBarWidget extends UIWidget implements GeckoSession.NavigationDelegate, GeckoSession.ProgressDelegate, GeckoSession.ContentDelegate, WidgetManagerDelegate.WorldClickListener, - WidgetManagerDelegate.UpdateListener, SessionStore.SessionChangeListener, + WidgetManagerDelegate.UpdateListener, SessionChangeListener, NavigationURLBar.NavigationURLBarDelegate, VoiceSearchWidget.VoiceSearchDelegate, SharedPreferences.OnSharedPreferenceChangeListener, SuggestionsWidget.URLBarPopupDelegate, BookmarkListener, TrayListener { @@ -62,7 +65,7 @@ public class NavigationBarWidget extends UIWidget implements GeckoSession.Naviga private ViewGroup mNavigationContainer; private ViewGroup mFullScreenModeContainer; private ViewGroup mResizeModeContainer; - private WindowWidget mWindowWidget; + private WindowWidget mAttachedWindow; private boolean mIsLoading; private boolean mIsInFullScreenMode; private boolean mIsResizing; @@ -80,6 +83,7 @@ public class NavigationBarWidget extends UIWidget implements GeckoSession.Naviga private UIButton mProjectionButton; private UITextButton mPreset0; private UITextButton mPreset1; + private UITextButton mPreset15; private UITextButton mPreset2; private UITextButton mPreset3; private ArrayList mButtons; @@ -94,6 +98,7 @@ public class NavigationBarWidget extends UIWidget implements GeckoSession.Naviga private MediaControlsWidget mMediaControlsWidget; private Media mFullScreenMedia; private @VideoProjectionMenuWidget.VideoProjectionFlags Integer mAutoSelectedProjection; + private SessionStack mSessionStack; public NavigationBarWidget(Context aContext) { super(aContext); @@ -110,7 +115,7 @@ public NavigationBarWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) initialize(aContext); } - private void initialize(Context aContext) { + private void initialize(@NonNull Context aContext) { mAppContext = aContext.getApplicationContext(); inflate(aContext, R.layout.navigation_bar, this); @@ -143,10 +148,9 @@ private void initialize(Context aContext) { mBackButton.setOnClickListener(v -> { v.requestFocusFromTouch(); - if (SessionStore.get().canGoBack()) - SessionStore.get().goBack(); - else if (SessionStore.get().canUnstackSession()) - SessionStore.get().unstackSession(); + + if (mSessionStack.canGoBack()) + mSessionStack.goBack(); if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.BACK); @@ -155,7 +159,7 @@ else if (SessionStore.get().canUnstackSession()) mForwardButton.setOnClickListener(v -> { v.requestFocusFromTouch(); - SessionStore.get().goForward(); + mSessionStack.goForward(); if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } @@ -164,12 +168,12 @@ else if (SessionStore.get().canUnstackSession()) mReloadButton.setOnClickListener(v -> { v.requestFocusFromTouch(); SessionStore.get().getHistoryStore().addHistory( - SessionStore.get().getCurrentUri(), + mAttachedWindow.getSessionStack().getCurrentUri(), VisitType.RELOAD); if (mIsLoading) { - SessionStore.get().stop(); + mSessionStack.stop(); } else { - SessionStore.get().reload(); + mSessionStack.reload(); } if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); @@ -178,7 +182,7 @@ else if (SessionStore.get().canUnstackSession()) mHomeButton.setOnClickListener(v -> { v.requestFocusFromTouch(); - SessionStore.get().loadUri(SessionStore.get().getHomeUri()); + mSessionStack.loadUri(mSessionStack.getHomeUri()); if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } @@ -186,7 +190,7 @@ else if (SessionStore.get().canUnstackSession()) mServoButton.setOnClickListener(v -> { v.requestFocusFromTouch(); - SessionStore.get().toggleServo(); + mSessionStack.toggleServo(); if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } @@ -196,6 +200,7 @@ else if (SessionStore.get().canUnstackSession()) mResizeExitButton = findViewById(R.id.resizeExitButton); mPreset0 = findViewById(R.id.resizePreset0); mPreset1 = findViewById(R.id.resizePreset1); + mPreset15 = findViewById(R.id.resizePreset15); mPreset2 = findViewById(R.id.resizePreset2); mPreset3 = findViewById(R.id.resizePreset3); @@ -280,6 +285,14 @@ else if (SessionStore.get().canUnstackSession()) } }); + mPreset15.setOnClickListener(view -> { + view.requestFocusFromTouch(); + setResizePreset(1.5f); + if (mAudio != null) { + mAudio.playSound(AudioEngine.Sound.CLICK); + } + }); + mPreset2.setOnClickListener(view -> { view.requestFocusFromTouch(); setResizePreset(2.0f); @@ -303,9 +316,6 @@ else if (SessionStore.get().canUnstackSession()) mURLBar.setDelegate(this); - SessionStore.get().addNavigationListener(this); - SessionStore.get().addProgressListener(this); - SessionStore.get().addContentListener(this); mWidgetManager.addUpdateListener(this); mWidgetManager.addWorldClickListener(this); @@ -314,13 +324,8 @@ else if (SessionStore.get().canUnstackSession()) mSuggestionsProvider = new SuggestionsProvider(getContext()); - SessionStore.get().addSessionChangeListener(this); - mPrefs = PreferenceManager.getDefaultSharedPreferences(mAppContext); mPrefs.registerOnSharedPreferenceChangeListener(this); - updateServoButton(); - - handleSessionState(); } @Override @@ -340,11 +345,10 @@ public void releaseWidget() { mWidgetManager.removeUpdateListener(this); mWidgetManager.removeWorldClickListener(this); mPrefs.unregisterOnSharedPreferenceChangeListener(this); - SessionStore.get().removeNavigationListener(this); - SessionStore.get().removeProgressListener(this); - SessionStore.get().removeContentListener(this); - SessionStore.get().removeSessionChangeListener(this); - mWindowWidget = null; + + detachFromWindow(); + + mAttachedWindow = null; super.releaseWidget(); } @@ -363,31 +367,70 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { } @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); + public void detachFromWindow() { + if (mIsResizing) { + exitResizeMode(ResizeAction.RESTORE_SIZE); + } + if (mIsInFullScreenMode) { + exitFullScreenMode(); + } + if (mURLBar.isInBookmarkMode() && mAttachedWindow != null) { + onBookmarksHidden(mAttachedWindow); + } + if (mSessionStack != null) { + mSessionStack.removeSessionChangeListener(this); + mSessionStack.removeNavigationListener(this); + mSessionStack.removeProgressListener(this); + mSessionStack.removeContentListener(this); + mSessionStack = null; + } + if (mAttachedWindow != null) + mAttachedWindow.removeBookmarksListener(this); + mAttachedWindow = null; } - public void setBrowserWidget(WindowWidget aWidget) { - if (aWidget != null) { - mWidgetPlacement.parentHandle = aWidget.getHandle(); + @Override + public void attachToWindow(@NonNull WindowWidget aWindow) { + if (aWindow == mAttachedWindow) { + return; + } + detachFromWindow(); + + mWidgetPlacement.parentHandle = aWindow.getHandle(); + mAttachedWindow = aWindow; + mAttachedWindow.addBookmarksListener(this); + + mSessionStack = aWindow.getSessionStack(); + if (mSessionStack != null) { + mSessionStack.addSessionChangeListener(this); + mSessionStack.addNavigationListener(this); + mSessionStack.addProgressListener(this); + mSessionStack.addContentListener(this); + mURLBar.setSessionStack(mSessionStack); + updateServoButton(); + handleSessionState(); } - mWindowWidget = aWidget; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); } private void setFullScreenSize() { - mPlacementBeforeResize.copyFrom(mWindowWidget.getPlacement()); + mPlacementBeforeResize.copyFrom(mAttachedWindow.getPlacement()); final float minScale = WidgetPlacement.floatDimension(getContext(), R.dimen.window_fullscreen_min_scale); // Set browser fullscreen size float aspect = SettingsStore.getInstance(getContext()).getWindowAspect(); - Media media = SessionStore.get().getFullScreenVideo(); + Media media = mSessionStack.getFullScreenVideo(); if (media != null && media.getWidth() > 0 && media.getHeight() > 0) { aspect = (float)media.getWidth() / (float)media.getHeight(); } - float scale = mWindowWidget.getCurrentScale(); + float scale = mAttachedWindow.getCurrentScale(); // Enforce min fullscreen size. // If current window area is larger only resize if the aspect changes (e.g. media). - if (scale < minScale || aspect != mWindowWidget.getCurrentAspect()) { - mWindowWidget.resizeByMultiplier(aspect, Math.max(scale, minScale)); + if (scale < minScale || aspect != mAttachedWindow.getCurrentAspect()) { + mAttachedWindow.resizeByMultiplier(aspect, Math.max(scale, minScale)); } } @@ -396,7 +439,6 @@ private void enterFullScreenMode() { return; } - mWindowWidget.setSaveResizeChanges(false); setFullScreenSize(); mWidgetManager.pushBackHandler(mFullScreenBackHandler); mIsInFullScreenMode = true; @@ -416,7 +458,7 @@ private void enterFullScreenMode() { mProjectionMenu.setDelegate((projection )-> { if (mIsInVRVideo) { // Reproject while reproducing VRVideo - mWidgetManager.showVRVideo(mWindowWidget.getHandle(), projection); + mWidgetManager.showVRVideo(mAttachedWindow.getHandle(), projection); closeFloatingMenus(); } else { enterVRVideo(projection); @@ -440,14 +482,13 @@ private void exitFullScreenMode() { // We need to add a delay for the exitFullScreen() call to solve some viewport scaling issues, // See https://github.com/MozillaReality/FirefoxReality/issues/833 for more info. postDelayed(() -> { - if (SessionStore.get().isInFullScreen()) { - SessionStore.get().exitFullScreen(); + if (mSessionStack.isInFullScreen()) { + mSessionStack.exitFullScreen(); } }, 50); - mWindowWidget.getPlacement().copyFrom(mPlacementBeforeResize); - mWidgetManager.updateWidget(mWindowWidget); - mWindowWidget.setSaveResizeChanges(true); + mAttachedWindow.getPlacement().copyFrom(mPlacementBeforeResize); + mWidgetManager.updateWidget(mAttachedWindow); mIsInFullScreenMode = false; mWidgetManager.popBackHandler(mFullScreenBackHandler); @@ -467,7 +508,7 @@ private void enterResizeMode() { return; } mIsResizing = true; - mPlacementBeforeResize.copyFrom(mWindowWidget.getPlacement()); + mPlacementBeforeResize.copyFrom(mAttachedWindow.getPlacement()); startWidgetResize(); AnimationHelper.fadeIn(mResizeModeContainer, AnimationHelper.FADE_ANIMATION_DURATION, null); if (mIsInFullScreenMode) { @@ -478,6 +519,14 @@ private void enterResizeMode() { mWidgetManager.pushBackHandler(mResizeBackHandler); mWidgetManager.setTrayVisible(false); closeFloatingMenus(); + + float maxScale = 3.0f; + if (mAttachedWindow != null) { + maxScale = mAttachedWindow.getMaxWindowScale(); + } + mPreset3.setEnabled(maxScale >= 3.0f); + mPreset2.setEnabled(maxScale >= 2.0f); + mPreset15.setVisibility(maxScale == 1.5f ? View.VISIBLE: View.GONE); } @@ -491,9 +540,8 @@ private void exitResizeMode(ResizeAction aResizeAction) { return; } if (aResizeAction == ResizeAction.RESTORE_SIZE) { - mWindowWidget.getPlacement().copyFrom(mPlacementBeforeResize); - mWidgetManager.updateWidget(mWindowWidget); - mWindowWidget.saveCurrentSize(); + mAttachedWindow.getPlacement().copyFrom(mPlacementBeforeResize); + mWidgetManager.updateWidget(mAttachedWindow); } mIsResizing = false; finishWidgetResize(); @@ -506,6 +554,9 @@ private void exitResizeMode(ResizeAction aResizeAction) { mWidgetManager.popBackHandler(mResizeBackHandler); mWidgetManager.setTrayVisible(!mIsInFullScreenMode); closeFloatingMenus(); + + if (aResizeAction == ResizeAction.KEEP_SIZE) + TelemetryWrapper.windowsResizeEvent(); } private void enterVRVideo(@VideoProjectionMenuWidget.VideoProjectionFlags int aProjection) { @@ -518,19 +569,19 @@ private void enterVRVideo(@VideoProjectionMenuWidget.VideoProjectionFlags int aP // Backup the placement because the same widget is reused in FullScreen & MediaControl menus mProjectionMenuPlacement.copyFrom(mProjectionMenu.getPlacement()); - mFullScreenMedia = SessionStore.get().getFullScreenVideo(); + mFullScreenMedia = mSessionStack.getFullScreenVideo(); this.setVisible(false); if (mFullScreenMedia != null && mFullScreenMedia.getWidth() > 0 && mFullScreenMedia.getHeight() > 0) { final boolean resetBorder = aProjection == VideoProjectionMenuWidget.VIDEO_PROJECTION_360 || aProjection == VideoProjectionMenuWidget.VIDEO_PROJECTION_360_STEREO; - mWindowWidget.enableVRVideoMode(mFullScreenMedia.getWidth(), mFullScreenMedia.getHeight(), resetBorder); + mAttachedWindow.enableVRVideoMode(mFullScreenMedia.getWidth(), mFullScreenMedia.getHeight(), resetBorder); // Handle video resize while in VR video playback mFullScreenMedia.setResizeDelegate((width, height) -> { - mWindowWidget.enableVRVideoMode(width, height, resetBorder); + mAttachedWindow.enableVRVideoMode(width, height, resetBorder); }); } - mWindowWidget.setVisible(false); + mAttachedWindow.setVisible(false); closeFloatingMenus(); if (aProjection != VideoProjectionMenuWidget.VIDEO_PROJECTION_3D_SIDE_BY_SIDE) { @@ -539,7 +590,7 @@ private void enterVRVideo(@VideoProjectionMenuWidget.VideoProjectionFlags int aP if (mMediaControlsWidget == null) { mMediaControlsWidget = new MediaControlsWidget(getContext()); - mMediaControlsWidget.setParentWidget(mWindowWidget.getHandle()); + mMediaControlsWidget.setParentWidget(mAttachedWindow.getHandle()); mMediaControlsWidget.getPlacement().visible = false; mWidgetManager.addWidget(mMediaControlsWidget); mMediaControlsWidget.setBackHandler(this::exitVRVideo); @@ -548,7 +599,7 @@ private void enterVRVideo(@VideoProjectionMenuWidget.VideoProjectionFlags int aP mMediaControlsWidget.setMedia(mFullScreenMedia); mMediaControlsWidget.setProjectionSelectorEnabled(true); mWidgetManager.updateWidget(mMediaControlsWidget); - mWidgetManager.showVRVideo(mWindowWidget.getHandle(), aProjection); + mWidgetManager.showVRVideo(mAttachedWindow.getHandle(), aProjection); } private void exitVRVideo() { @@ -566,14 +617,14 @@ private void exitVRVideo() { mWidgetManager.setControllersVisible(true); this.setVisible(true); - mWindowWidget.disableVRVideoMode(); - mWindowWidget.setVisible(true); + mAttachedWindow.disableVRVideoMode(); + mAttachedWindow.setVisible(true); mMediaControlsWidget.setVisible(false); } private void setResizePreset(float aMultiplier) { final float aspect = SettingsStore.getInstance(getContext()).getWindowAspect(); - mWindowWidget.resizeByMultiplier(aspect, aMultiplier); + mAttachedWindow.resizeByMultiplier(aspect, aMultiplier); } public void showVoiceSearch() { @@ -586,19 +637,21 @@ public void updateServoButton() { // 2. Or, if the pref is enabled and the current url is white listed. boolean show = false; boolean isServoSession = false; - GeckoSession currentSession = SessionStore.get().getCurrentSession(); - if (currentSession != null) { - String currentUri = SessionStore.get().getCurrentUri(); - boolean isPrefEnabled = SettingsStore.getInstance(mAppContext).isServoEnabled(); - boolean isUrlWhiteListed = ServoUtils.isUrlInServoWhiteList(mAppContext, currentUri); - isServoSession = ServoUtils.isInstanceOfServoSession(currentSession); - show = isServoSession || (isPrefEnabled && isUrlWhiteListed); - } - if (show) { - mServoButton.setVisibility(View.VISIBLE); - mServoButton.setImageResource(isServoSession ? R.drawable.ic_icon_gecko : R.drawable.ic_icon_servo); - } else { - mServoButton.setVisibility(View.GONE); + if (mSessionStack != null){ + GeckoSession currentSession = mSessionStack.getCurrentSession(); + if (currentSession != null) { + String currentUri = mSessionStack.getCurrentUri(); + boolean isPrefEnabled = SettingsStore.getInstance(mAppContext).isServoEnabled(); + boolean isUrlWhiteListed = ServoUtils.isUrlInServoWhiteList(mAppContext, currentUri); + isServoSession = ServoUtils.isInstanceOfServoSession(currentSession); + show = isServoSession || (isPrefEnabled && isUrlWhiteListed); + } + if (show) { + mServoButton.setVisibility(View.VISIBLE); + mServoButton.setImageResource(isServoSession ? R.drawable.ic_icon_gecko : R.drawable.ic_icon_servo); + } else { + mServoButton.setVisibility(View.GONE); + } } } @@ -612,32 +665,26 @@ private void closeFloatingMenus() { } private void handleSessionState() { - boolean isPrivateMode = SessionStore.get().isCurrentSessionPrivate(); + if (mSessionStack != null) { + boolean isPrivateMode = mSessionStack.isPrivateMode(); - mURLBar.setPrivateMode(isPrivateMode); - for (CustomUIButton button : mButtons) { - button.setPrivateMode(isPrivateMode); + mURLBar.setPrivateMode(isPrivateMode); + for (CustomUIButton button : mButtons) { + button.setPrivateMode(isPrivateMode); + } } } - @Override - public GeckoResult onNewSession(@NonNull GeckoSession aSession, @NonNull String aUri) { - return null; - } - - @Override - public GeckoResult onLoadError(GeckoSession session, String uri, WebRequestError error) { - return null; - } - public void release() { - SessionStore.get().removeNavigationListener(this); - SessionStore.get().removeProgressListener(this); + if (mSessionStack != null) { + mSessionStack.removeNavigationListener(this); + mSessionStack.removeProgressListener(this); + } } @Override public void onLocationChange(GeckoSession session, String url) { - if (mURLBar != null) { + if (mURLBar != null && !mAttachedWindow.isBookmarksVisible()) { Log.d(LOGTAG, "Got location change"); mURLBar.setURL(url); mReloadButton.setEnabled(true); @@ -648,7 +695,7 @@ public void onLocationChange(GeckoSession session, String url) { @Override public void onCanGoBack(GeckoSession aSession, boolean canGoBack) { if (mBackButton != null) { - boolean enableBackButton = SessionStore.get().canUnstackSession() | canGoBack; + boolean enableBackButton = mSessionStack.canGoBack(); Log.d(LOGTAG, "Got onCanGoBack: " + (enableBackButton ? "true" : "false")); mBackButton.setEnabled(enableBackButton); @@ -720,11 +767,6 @@ public void onPageStop(GeckoSession aSession, boolean b) { } } - @Override - public void onProgressChange(GeckoSession session, int progress) { - - } - @Override public void onSecurityChange(GeckoSession geckoSession, SecurityInformation securityInformation) { if (mURLBar != null) { @@ -735,21 +777,6 @@ public void onSecurityChange(GeckoSession geckoSession, SecurityInformation secu // Content delegate - @Override - public void onTitleChange(GeckoSession session, String title) { - - } - - @Override - public void onFocusRequest(GeckoSession session) { - - } - - @Override - public void onCloseRequest(GeckoSession session) { - - } - @Override public void onFullScreen(GeckoSession session, boolean aFullScreen) { if (aFullScreen) { @@ -760,7 +787,7 @@ public void onFullScreen(GeckoSession session, boolean aFullScreen) { exitResizeMode(ResizeAction.KEEP_SIZE); } AtomicBoolean autoEnter = new AtomicBoolean(false); - mAutoSelectedProjection = VideoProjectionMenuWidget.getAutomaticProjection(SessionStore.get().getUriFromSession(session), autoEnter); + mAutoSelectedProjection = VideoProjectionMenuWidget.getAutomaticProjection(mSessionStack.getUriFromSession(session), autoEnter); if (mAutoSelectedProjection != null && autoEnter.get()) { mAutoEnteredVRVideo = true; postDelayed(() -> enterVRVideo(mAutoSelectedProjection), 300); @@ -775,30 +802,10 @@ public void onFullScreen(GeckoSession session, boolean aFullScreen) { } } - @Override - public void onContextMenu(GeckoSession session, int screenX, int screenY, ContextElement element) { - - } - - @Override - public void onExternalResponse(GeckoSession session, GeckoSession.WebResponseInfo response) { - - } - - @Override - public void onCrash(GeckoSession session) { - - } - - @Override - public void onFirstComposite(GeckoSession session) { - - } - // WidgetManagerDelegate.UpdateListener @Override public void onWidgetUpdate(Widget aWidget) { - if (aWidget != mWindowWidget || mIsResizing) { + if (aWidget != mAttachedWindow || mIsResizing) { return; } @@ -814,22 +821,13 @@ public void onWidgetUpdate(Widget aWidget) { mWidgetManager.updateWidget(this); } - // SessionStore.SessionChangeListener - @Override - public void onNewSession(GeckoSession aSession, int aId) { - - } - - @Override - public void onRemoveSession(GeckoSession aSession, int aId) { - - } + // SessionStack.SessionChangeListener @Override public void onCurrentSessionChange(GeckoSession aSession, int aId) { handleSessionState(); - boolean isFullScreen = SessionStore.get().isInFullScreen(aSession); + boolean isFullScreen = mSessionStack.isInFullScreen(aSession); if (isFullScreen && !mIsInFullScreenMode) { enterFullScreenMode(); } else if (!isFullScreen && mIsInFullScreenMode) { @@ -894,16 +892,6 @@ public void OnVoiceSearchResult(String transcription, float confidance) { mURLBar.handleURLEdit(transcription); } - @Override - public void OnVoiceSearchCanceled() { - // Nothing to do yet - } - - @Override - public void OnVoiceSearchError() { - // Nothing to do yet - } - @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key == mAppContext.getString(R.string.settings_key_servo)) { @@ -943,25 +931,24 @@ public void OnItemClicked(SuggestionsWidget.SuggestionItem item) { mURLBar.handleURLEdit(item.url); } - @Override - public void OnItemDeleted(SuggestionsWidget.SuggestionItem item) { - - } - // BookmarkListener @Override - public void onBookmarksShown() { - mURLBar.setURL(""); - mURLBar.setHint(R.string.about_bookmarks); - mURLBar.setIsBookmarkMode(true); + public void onBookmarksShown(WindowWidget aWindow) { + if (mAttachedWindow == aWindow) { + mURLBar.setURL(""); + mURLBar.setHint(R.string.about_bookmarks); + mURLBar.setIsBookmarkMode(true); + } } @Override - public void onBookmarksHidden() { - mURLBar.setIsBookmarkMode(false); - mURLBar.setURL(SessionStore.get().getCurrentUri()); - mURLBar.setHint(R.string.search_placeholder); + public void onBookmarksHidden(WindowWidget aWindow) { + if (mAttachedWindow == aWindow) { + mURLBar.setIsBookmarkMode(false); + mURLBar.setURL(mSessionStack.getCurrentUri()); + mURLBar.setHint(R.string.search_placeholder); + } } // TrayListener @@ -985,14 +972,18 @@ public void onPrivateBrowsingClicked() { } private void finishWidgetResize() { - mWidgetManager.finishWidgetResize(mWindowWidget); + mWidgetManager.finishWidgetResize(mAttachedWindow); } private void startWidgetResize() { - mWidgetManager.startWidgetResize(mWindowWidget); + if (mAttachedWindow != null) { + Pair targetSize = mAttachedWindow.getSizeForScale(mAttachedWindow.getMaxWindowScale()); + mWidgetManager.startWidgetResize(mAttachedWindow, targetSize.first, 4.5f); + } } private void updateWidget() { - onWidgetUpdate(mWindowWidget); + onWidgetUpdate(mAttachedWindow); } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NoInternetWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NoInternetWidget.java index 5c6f281b9..c89335072 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NoInternetWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NoInternetWidget.java @@ -7,14 +7,10 @@ import android.content.Context; import android.util.AttributeSet; -import android.view.View; import android.widget.Button; -import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; -import org.mozilla.vrbrowser.browser.SessionStore; -import org.mozilla.vrbrowser.ui.views.UIButton; public class NoInternetWidget extends UIWidget { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/SuggestionsWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/SuggestionsWidget.java index a0290fc8f..b57f53172 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/SuggestionsWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/SuggestionsWidget.java @@ -38,8 +38,8 @@ public class SuggestionsWidget extends UIWidget implements WidgetManagerDelegate private AudioEngine mAudio; public interface URLBarPopupDelegate { - void OnItemClicked(SuggestionItem item); - void OnItemDeleted(SuggestionItem item); + default void OnItemClicked(SuggestionItem item) {}; + default void OnItemDeleted(SuggestionItem item) {}; } public SuggestionsWidget(Context aContext) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TopBarWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TopBarWidget.java index 5e0cf68e9..3b3275f7d 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TopBarWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TopBarWidget.java @@ -8,17 +8,24 @@ import android.content.Context; import android.util.AttributeSet; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; -import org.mozilla.vrbrowser.browser.SessionStore; +import org.mozilla.vrbrowser.browser.SessionChangeListener; import org.mozilla.vrbrowser.ui.views.UIButton; -public class TopBarWidget extends UIWidget implements SessionStore.SessionChangeListener, WidgetManagerDelegate.UpdateListener { +public class TopBarWidget extends UIWidget implements SessionChangeListener, WidgetManagerDelegate.UpdateListener { private UIButton mCloseButton; + private UIButton mMoveLeftButton; + private UIButton mMoveRightButton; private AudioEngine mAudio; - private UIWidget mBrowserWidget; + private WindowWidget mAttachedWindow; + private TopBarWidget.Delegate mDelegate; + private boolean mVisible; public TopBarWidget(Context aContext) { super(aContext); @@ -35,24 +42,53 @@ public TopBarWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { initialize(aContext); } + public interface Delegate { + void onCloseClicked(TopBarWidget aWidget); + void onMoveLeftClicked(TopBarWidget aWidget); + void onMoveRightClicked(TopBarWidget aWidget); + } + private void initialize(Context aContext) { inflate(aContext, R.layout.top_bar, this); - mCloseButton = findViewById(R.id.negativeButton); + mCloseButton = findViewById(R.id.closeWindowButton); mCloseButton.setOnClickListener(view -> { view.requestFocusFromTouch(); if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } - SessionStore.get().exitPrivateMode(); + if (mDelegate != null) { + mDelegate.onCloseClicked(TopBarWidget.this); + } + }); + + mMoveLeftButton = findViewById(R.id.moveWindowLeftButton); + mMoveLeftButton.setOnClickListener(view -> { + view.requestFocusFromTouch(); + if (mAudio != null) { + mAudio.playSound(AudioEngine.Sound.CLICK); + } + if (mDelegate != null) { + mDelegate.onMoveLeftClicked(TopBarWidget.this); + } }); + mMoveRightButton = findViewById(R.id.moveWindowRightButton); + mMoveRightButton.setOnClickListener(view -> { + view.requestFocusFromTouch(); + if (mAudio != null) { + mAudio.playSound(AudioEngine.Sound.CLICK); + } + if (mDelegate != null) { + mDelegate.onMoveRightClicked(TopBarWidget.this); + } + }); + + + mAudio = AudioEngine.fromContext(aContext); - SessionStore.get().addSessionChangeListener(this); mWidgetManager.addUpdateListener(this); - - handleSessionState(); } @Override @@ -61,7 +97,7 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.width = WidgetPlacement.dpDimension(context, R.dimen.top_bar_width); aPlacement.height = WidgetPlacement.dpDimension(context, R.dimen.top_bar_height); aPlacement.worldWidth = WidgetPlacement.floatDimension(getContext(), R.dimen.window_world_width) * aPlacement.width/getWorldWidth(); - //aPlacement.translationY = WidgetPlacement.unitFromMeters(context, R.dimen.top_bar_world_y); + aPlacement.translationY = WidgetPlacement.dpDimension(context, R.dimen.top_bar_window_margin); aPlacement.anchorX = 0.5f; aPlacement.anchorY = 0.0f; aPlacement.parentAnchorX = 0.5f; @@ -71,69 +107,88 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { @Override public void releaseWidget() { - SessionStore.get().removeSessionChangeListener(this); mWidgetManager.removeUpdateListener(this); super.releaseWidget(); } - public void setBrowserWidget(UIWidget aWidget) { - if (aWidget != null) { - mWidgetPlacement.parentHandle = aWidget.getHandle(); - } - mBrowserWidget = aWidget; + @Override + public void detachFromWindow() { + mAttachedWindow = null; } - // SessionStore.SessionChangeListener - @Override - public void onNewSession(GeckoSession aSession, int aId) { + public void attachToWindow(@NonNull WindowWidget aWindow) { + if (mAttachedWindow == aWindow) { + return; + } + mWidgetPlacement.parentHandle = aWindow.getHandle(); + mAttachedWindow = aWindow; + + setPrivateMode(aWindow.getSessionStack().isPrivateMode()); } - @Override - public void onRemoveSession(GeckoSession aSession, int aId) { + public @Nullable WindowWidget getAttachedWindow() { + return mAttachedWindow; + } + private void setPrivateMode(boolean aPrivateMode) { + mCloseButton.setPrivateMode(aPrivateMode); + mMoveLeftButton.setPrivateMode(aPrivateMode); + mMoveRightButton.setPrivateMode(aPrivateMode); + mCloseButton.setBackground(getContext().getDrawable(aPrivateMode ? R.drawable.fullscreen_button_private : R.drawable.fullscreen_button)); + mMoveLeftButton.setBackground(getContext().getDrawable(aPrivateMode ? R.drawable.fullscreen_button_private_first : R.drawable.fullscreen_button_first)); + mMoveRightButton.setBackground(getContext().getDrawable(aPrivateMode ? R.drawable.fullscreen_button_private_last : R.drawable.fullscreen_button_last)); } + // SessionStack.SessionChangeListener + @Override public void onCurrentSessionChange(GeckoSession aSession, int aId) { handleSessionState(); } @Override - public void setVisible(boolean isVisible) { - getPlacement().visible = isVisible; + public void setVisible(boolean aIsVisible) { + if (mVisible == aIsVisible) { + return; + } + mVisible = aIsVisible; + getPlacement().visible = aIsVisible; - if (isVisible) + if (aIsVisible) { mWidgetManager.addWidget(this); - else + } + else { mWidgetManager.removeWidget(this); + } + } + + public void setDelegate(TopBarWidget.Delegate aDelegate) { + mDelegate = aDelegate; + } + + public void setMoveLeftButtonEnabled(boolean aEnabled) { + mMoveRightButton.setHovered(false); + mMoveLeftButton.setEnabled(aEnabled); + } + + public void setMoveRightButtonEnabled(boolean aEnabled) { + mMoveLeftButton.setHovered(false); + mMoveRightButton.setEnabled(aEnabled); } private void handleSessionState() { - boolean isPrivateMode = SessionStore.get().isCurrentSessionPrivate(); - setVisible(isPrivateMode); - mCloseButton.setPrivateMode(isPrivateMode); } // WidgetManagerDelegate.UpdateListener + @Override public void onWidgetUpdate(Widget aWidget) { - if (aWidget != mBrowserWidget) { + if (aWidget != mAttachedWindow) { return; } - - if (mBrowserWidget.isVisible()) { - boolean isVisible = isVisible(); - boolean mustBeVisible = SessionStore.get().isCurrentSessionPrivate(); - if (mustBeVisible && !isVisible) { - setVisible(true); - } else if (isVisible) { - mWidgetManager.updateWidget(this); - } - } else { - setVisible(false); - } } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayListener.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayListener.java index ec4030620..51b2cb9f9 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayListener.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayListener.java @@ -1,6 +1,7 @@ package org.mozilla.vrbrowser.ui.widgets; public interface TrayListener { - void onBookmarksClicked(); - void onPrivateBrowsingClicked(); + default void onBookmarksClicked() {}; + default void onPrivateBrowsingClicked() {}; + default void onAddWindowClicked() {}; } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayWidget.java index 1815b6190..33012964b 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayWidget.java @@ -14,10 +14,14 @@ import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; +import androidx.annotation.NonNull; + import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; -import org.mozilla.vrbrowser.browser.SessionStore; +import org.mozilla.vrbrowser.browser.SessionChangeListener; +import org.mozilla.vrbrowser.browser.engine.SessionStack; +import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.views.UIButton; import org.mozilla.vrbrowser.ui.widgets.settings.SettingsWidget; @@ -25,7 +29,7 @@ import java.util.Arrays; import java.util.List; -public class TrayWidget extends UIWidget implements SessionStore.SessionChangeListener, BookmarkListener, WidgetManagerDelegate.UpdateListener { +public class TrayWidget extends UIWidget implements SessionChangeListener, BookmarkListener, WidgetManagerDelegate.UpdateListener { static final String LOGTAG = "VRB"; private static final int ICON_ANIMATION_DURATION = 200; @@ -40,6 +44,8 @@ public class TrayWidget extends UIWidget implements SessionStore.SessionChangeLi private int mMaxPadding; private boolean mKeyboardVisible; private boolean mTrayVisible = true; + private SessionStack mSessionStack; + private WindowWidget mAttachedWindow; public TrayWidget(Context aContext) { super(aContext); @@ -73,8 +79,6 @@ private void initialize(Context aContext) { notifyPrivateBrowsingClicked(); view.requestFocusFromTouch(); - - SessionStore.get().switchPrivateMode(); }); mSettingsButton = findViewById(R.id.settingsButton); @@ -100,13 +104,24 @@ private void initialize(Context aContext) { view.requestFocusFromTouch(); }); + UIButton addWindowButton = findViewById(R.id.addwindowButton); + addWindowButton.setOnHoverListener(mButtonScaleHoverListener); + addWindowButton.setOnClickListener(view -> { + if (mAudio != null) { + mAudio.playSound(AudioEngine.Sound.CLICK); + } + + view.requestFocusFromTouch(); + + TelemetryWrapper.trayNewWindowEvent(); + + notifyAddWindowClicked(); + }); + mAudio = AudioEngine.fromContext(aContext); mIsLastSessionPrivate = false; - SessionStore.get().addSessionChangeListener(this); - - handleSessionState(); mWidgetManager.addUpdateListener(this); } @@ -172,16 +187,24 @@ public void addListeners(TrayListener... listeners) { mTrayListeners.addAll(Arrays.asList(listeners)); } + public void removeListeners(TrayListener... listeners) { + mTrayListeners.removeAll(Arrays.asList(listeners)); + } + public void onDestroy() { mTrayListeners.clear(); } private void notifyBookmarksClicked() { - mTrayListeners.forEach(trayListener -> trayListener.onBookmarksClicked()); + mTrayListeners.forEach(TrayListener::onBookmarksClicked); } private void notifyPrivateBrowsingClicked() { - mTrayListeners.forEach(trayListener -> trayListener.onPrivateBrowsingClicked()); + mTrayListeners.forEach(TrayListener::onPrivateBrowsingClicked); + } + + private void notifyAddWindowClicked() { + mTrayListeners.forEach(TrayListener::onAddWindowClicked); } @Override @@ -205,47 +228,94 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { @Override public void releaseWidget() { - SessionStore.get().removeSessionChangeListener(this); + if (mSessionStack != null) { + mSessionStack.removeSessionChangeListener(this); + } + mWidgetManager.removeUpdateListener(this); super.releaseWidget(); } - // SessionStore.SessionChangeListener + @Override + public void show(@ShowFlags int aShowFlags) { + if (!mWidgetPlacement.visible) { + mWidgetPlacement.visible = true; + mWidgetManager.addWidget(this); + } + } @Override - public void onNewSession(GeckoSession aSession, int aId) { + public void hide(@HideFlags int aHideFlags) { + if (mWidgetPlacement.visible) { + mWidgetPlacement.visible = false; + if (aHideFlags == REMOVE_WIDGET) { + mWidgetManager.removeWidget(this); + } else { + mWidgetManager.updateWidget(this); + } + } + } + @Override + public void detachFromWindow() { + if (mSessionStack != null) { + mSessionStack.removeSessionChangeListener(this); + mSessionStack = null; + } + if (mAttachedWindow != null) + mAttachedWindow.removeBookmarksListener(this); } @Override - public void onRemoveSession(GeckoSession aSession, int aId) { + public void attachToWindow(@NonNull WindowWidget aWindow) { + if (mAttachedWindow == aWindow) { + return; + } + detachFromWindow(); + mAttachedWindow = aWindow; + mAttachedWindow.addBookmarksListener(this); + + mSessionStack = aWindow.getSessionStack(); + if (mSessionStack != null) { + mSessionStack.addSessionChangeListener(this); + handleSessionState(); + } + + if (mAttachedWindow.isBookmarksVisible()) + onBookmarksShown(aWindow); + else + onBookmarksHidden(aWindow); } + // SessionStack.SessionChangeListener + @Override public void onCurrentSessionChange(GeckoSession aSession, int aId) { handleSessionState(); } private void handleSessionState() { - boolean isPrivateMode = SessionStore.get().isCurrentSessionPrivate(); - - if (isPrivateMode != mIsLastSessionPrivate) { - mPrivateButton.setPrivateMode(isPrivateMode); - if (isPrivateMode) { - mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); - mPrivateButton.setImageResource(R.drawable.ic_icon_private_browsing_on); + if (mSessionStack != null) { + boolean isPrivateMode = mSessionStack.isPrivateMode(); + + if (isPrivateMode != mIsLastSessionPrivate) { + mPrivateButton.setPrivateMode(isPrivateMode); + if (isPrivateMode) { + mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); + mPrivateButton.setImageResource(R.drawable.ic_icon_tray_private_browsing_on_v2); mPrivateButton.setTooltip(getResources().getString(R.string.private_browsing_exit_tooltip)); - } else { - mWidgetManager.popWorldBrightness(this); - mPrivateButton.setImageResource(R.drawable.ic_icon_private_browsing); + } else { + mWidgetManager.popWorldBrightness(this); + mPrivateButton.setImageResource(R.drawable.ic_icon_tray_private_browsing_v2); + } mPrivateButton.setTooltip(getResources().getString(R.string.private_browsing_enter_tooltip)); } - } - mIsLastSessionPrivate = isPrivateMode; + mIsLastSessionPrivate = isPrivateMode; + } } private void toggleSettingsDialog() { @@ -277,26 +347,6 @@ private void updateVisibility() { } } - @Override - public void show(@ShowFlags int aShowFlags) { - if (!mWidgetPlacement.visible) { - mWidgetPlacement.visible = true; - mWidgetManager.addWidget(this); - } - } - - @Override - public void hide(@HideFlags int aHideFlags) { - if (mWidgetPlacement.visible) { - mWidgetPlacement.visible = false; - if (aHideFlags == REMOVE_WIDGET) { - mWidgetManager.removeWidget(this); - } else { - mWidgetManager.updateWidget(this); - } - } - } - public boolean isDialogOpened(int aHandle) { UIWidget widget = getChild(aHandle); if (widget != null) { @@ -308,13 +358,13 @@ public boolean isDialogOpened(int aHandle) { // BookmarkListener @Override - public void onBookmarksShown() { + public void onBookmarksShown(WindowWidget aWindow) { mBookmarksButton.setTooltip(getResources().getString(R.string.close_bookmarks_tooltip)); mBookmarksButton.setActiveMode(true); } @Override - public void onBookmarksHidden() { + public void onBookmarksHidden(WindowWidget aWindow) { mBookmarksButton.setTooltip(getResources().getString(R.string.open_bookmarks_tooltip)); mBookmarksButton.setActiveMode(false); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java index 34241be0f..fae38dfb0 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java @@ -329,6 +329,11 @@ public void setVisible(boolean aVisible) { } } + @Override + public int getBorderWidth() { + return 0; + } + protected T createChild(@NonNull Class aChildClassName) { return createChild(aChildClassName, true); } @@ -371,4 +376,5 @@ protected void onDismiss() { protected float getWorldWidth() { return mWorldWidth; } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/VideoProjectionMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/VideoProjectionMenuWidget.java index d8a2713d6..b92517141 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/VideoProjectionMenuWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/VideoProjectionMenuWidget.java @@ -46,7 +46,7 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.anchorX = 0.0f; aPlacement.anchorY = 0.0f; aPlacement.translationY = WidgetPlacement.dpDimension(getContext(), R.dimen.video_projection_menu_translation_y); - aPlacement.translationZ = 1.0f; + aPlacement.translationZ = 30.0f; } public void setParentWidget(UIWidget aParent) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Widget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Widget.java index 6fb403c17..ac2be6b04 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Widget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Widget.java @@ -9,7 +9,12 @@ import android.view.MotionEvent; import android.view.Surface; +import androidx.annotation.NonNull; + public interface Widget { + + int NO_WINDOW_ID = -1; + void onPause(); void onResume(); void setSurfaceTexture(SurfaceTexture aTexture, final int aWidth, final int aHeight); @@ -28,4 +33,7 @@ public interface Widget { boolean isDialog(); void setVisible(boolean aVisible); void resizeByMultiplier(float aspect, float multiplier); + default void detachFromWindow() {} + default void attachToWindow(@NonNull WindowWidget window) {} + int getBorderWidth(); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java index 5c476527e..536f6c864 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java @@ -3,37 +3,50 @@ import android.view.View; import org.mozilla.geckoview.GeckoSession; +import org.mozilla.vrbrowser.VRBrowserActivity; import androidx.annotation.IntDef; import androidx.annotation.NonNull; public interface WidgetManagerDelegate { + interface UpdateListener { void onWidgetUpdate(Widget aWidget); } + interface PermissionListener { void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults); } + interface FocusChangeListener { void onGlobalFocusChanged(View oldFocus, View newFocus); } + interface WorldClickListener { // Indicates that the user has clicked on the world, outside of any UI widgets void onWorldClick(); } + float DEFAULT_DIM_BRIGHTNESS = 0.25f; float DEFAULT_NO_DIM_BRIGHTNESS = 1.0f; + @IntDef(value = { WIDGET_MOVE_BEHAVIOUR_GENERAL, WIDGET_MOVE_BEHAVIOUR_KEYBOARD}) public @interface WidgetMoveBehaviourFlags {} public static final int WIDGET_MOVE_BEHAVIOUR_GENERAL = 0; public static final int WIDGET_MOVE_BEHAVIOUR_KEYBOARD = 1; + @IntDef(value = { CPU_LEVEL_NORMAL, CPU_LEVEL_HIGH}) + @interface CPULevelFlags {} + int CPU_LEVEL_NORMAL = 0; + int CPU_LEVEL_HIGH = 1; + int newWidgetHandle(); void addWidget(@NonNull Widget aWidget); void updateWidget(@NonNull Widget aWidget); void removeWidget(@NonNull Widget aWidget); - void startWidgetResize(@NonNull Widget aWidget); + void updateVisibleWidgets(); + void startWidgetResize(@NonNull Widget aWidget, float maxWidth, float maxHeight); void finishWidgetResize(@NonNull Widget aWidget); void startWidgetMove(@NonNull Widget aWidget, @WidgetMoveBehaviourFlags int aMoveBehaviour); void finishWidgetMove(); @@ -56,6 +69,7 @@ interface WorldClickListener { void hideVRVideo(); void resetUIYaw(); void setCylinderDensity(float aDensity); + void setCPULevel(@VRBrowserActivity.CPULevelFlags int aCPULevel); void addFocusChangeListener(@NonNull FocusChangeListener aListener); void removeFocusChangeListener(@NonNull FocusChangeListener aListener); void addPermissionListener(PermissionListener aListener); @@ -64,4 +78,5 @@ interface WorldClickListener { void removeWorldClickListener(WorldClickListener aListener); boolean isPermissionGranted(@NonNull String permission); void requestPermission(String uri, @NonNull String permission, GeckoSession.PermissionDelegate.Callback aCallback); + void openNewWindow(@NonNull String uri); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetPlacement.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetPlacement.java index 7a15daff0..c42ed9202 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetPlacement.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetPlacement.java @@ -12,12 +12,17 @@ import androidx.annotation.NonNull; +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.browser.SettingsStore; + public class WidgetPlacement { static final float WORLD_DPI_RATIO = 2.0f/720.0f; private WidgetPlacement() {} public WidgetPlacement(Context aContext) { density = aContext.getResources().getDisplayMetrics().density; + // Default value + cylinderMapRadius = Math.abs(WidgetPlacement.floatDimension(aContext, R.dimen.window_world_z)); } public float density; @@ -41,8 +46,16 @@ public WidgetPlacement(Context aContext) { public boolean showPointer = true; public boolean firstDraw = false; public boolean layer = true; - public boolean cylinder = true; public float textureScale = 0.7f; + // Widget will be curved if enabled. + public boolean cylinder = true; + /* + * Flat surface placements are automatically mapped to curved coordinates. + * If a radius is set it's used for the automatic mapping of the yaw & angle when the + * cylinder is not centered on the X axis. + * See Widget::UpdateCylinderMatrix for more info. + */ + public float cylinderMapRadius; public WidgetPlacement clone() { WidgetPlacement w = new WidgetPlacement(); @@ -72,8 +85,9 @@ public void copyFrom(WidgetPlacement w) { this.showPointer = w.showPointer; this.firstDraw = w.firstDraw; this.layer = w.layer; - this.cylinder = w.cylinder; this.textureScale = w.textureScale; + this.cylinder = w.cylinder; + this.cylinderMapRadius = w.cylinderMapRadius; } public int textureWidth() { @@ -133,4 +147,8 @@ public static float worldToWidgetRatio(@NonNull UIWidget widget) { return ((widgetWorldWidth/widget.mWidgetPlacement.width)/WORLD_DPI_RATIO); } + public static float worldToWindowRatio(Context aContext){ + return (WidgetPlacement.floatDimension(aContext, R.dimen.window_world_width) / SettingsStore.WINDOW_WIDTH_DEFAULT)/ WORLD_DPI_RATIO; + } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java index b8ed903a8..c76651aa1 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java @@ -7,9 +7,11 @@ import android.content.Context; import android.graphics.Canvas; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.util.Log; +import android.util.Pair; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; @@ -18,27 +20,39 @@ import android.view.inputmethod.InputConnection; import org.mozilla.gecko.util.ThreadUtils; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.mozilla.geckoview.AllowOrDeny; import org.mozilla.geckoview.GeckoDisplay; import org.mozilla.geckoview.GeckoResult; import org.mozilla.geckoview.GeckoSession; -import org.mozilla.geckoview.GeckoSessionSettings; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.browser.SessionStore; +import org.mozilla.vrbrowser.browser.SessionChangeListener; import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.VideoAvailabilityListener; +import org.mozilla.vrbrowser.browser.engine.SessionStore; +import org.mozilla.vrbrowser.browser.engine.SessionStack; +import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.views.BookmarksView; +import org.mozilla.vrbrowser.ui.widgets.dialogs.ContextMenuWidget; +import org.mozilla.vrbrowser.ui.widgets.dialogs.MaxWindowsWidget; import org.mozilla.vrbrowser.ui.widgets.prompts.AlertPromptWidget; import org.mozilla.vrbrowser.ui.widgets.prompts.AuthPromptWidget; import org.mozilla.vrbrowser.ui.widgets.prompts.ChoicePromptWidget; import org.mozilla.vrbrowser.ui.widgets.prompts.ConfirmPromptWidget; import org.mozilla.vrbrowser.ui.widgets.prompts.TextPromptWidget; +import org.mozilla.vrbrowser.utils.InternalPages; -import androidx.annotation.NonNull; +import java.util.ArrayList; + +import mozilla.components.concept.storage.VisitType; import static org.mozilla.vrbrowser.utils.ServoUtils.isInstanceOfServoSession; -public class WindowWidget extends UIWidget implements SessionStore.SessionChangeListener, - GeckoSession.ContentDelegate, GeckoSession.PromptDelegate, TrayListener, BookmarkListener { +public class WindowWidget extends UIWidget implements SessionChangeListener, + GeckoSession.ContentDelegate, GeckoSession.PromptDelegate, + GeckoSession.NavigationDelegate, VideoAvailabilityListener { private static final String LOGTAG = "VRB"; @@ -49,58 +63,74 @@ public class WindowWidget extends UIWidget implements SessionStore.SessionChange private int mHeight; private int mHandle; private WidgetPlacement mWidgetPlacement; + private TopBarWidget mTopBar; private WidgetManagerDelegate mWidgetManager; private ChoicePromptWidget mChoicePrompt; private AlertPromptWidget mAlertPrompt; + private MaxWindowsWidget mMaxWindowsDialog; private ConfirmPromptWidget mConfirmPrompt; private TextPromptWidget mTextPrompt; private AuthPromptWidget mAuthPrompt; private NoInternetWidget mNoInternetToast; + private ContextMenuWidget mContextMenu; private int mWidthBackup; private int mHeightBackup; private int mBorderWidth; - Runnable mFirstDrawCallback; + private Runnable mFirstDrawCallback; private boolean mIsInVRVideoMode; - private boolean mSaveResizeChanges; private View mView; + private Point mLastMouseClickPos; + private SessionStack mSessionStack; + private int mWindowId; private BookmarksView mBookmarksView; + private ArrayList mBookmarksListeners; + private Windows.WindowPlacement mWindowPlacement = Windows.WindowPlacement.FRONT; + private float mMaxWindowScale = 3; + private boolean mIsRestored = false; + boolean mActive = false; - public WindowWidget(Context aContext, int aSessionId) { + public WindowWidget(Context aContext, int windowId, boolean privateMode) { super(aContext); - mSessionId = aSessionId; mWidgetManager = (WidgetManagerDelegate) aContext; mBorderWidth = SettingsStore.getInstance(aContext).getTransparentBorderWidth(); - SessionStore.get().addSessionChangeListener(this); - SessionStore.get().addPromptListener(this); - SessionStore.get().addContentListener(this); - setFocusable(true); - GeckoSession session = SessionStore.get().getSession(mSessionId); - if (session != null) { - session.getTextInput().setView(this); - } + + mWindowId = windowId; + mSessionStack = SessionStore.get().createSessionStack(mWindowId, privateMode); + mSessionStack.addSessionChangeListener(this); + mSessionStack.addPromptListener(this); + mSessionStack.addContentListener(this); + mSessionStack.addVideoAvailabilityListener(this); + mSessionStack.addNavigationListener(this); + mSessionStack.newSession(); + + mBookmarksView = new BookmarksView(aContext); + mBookmarksListeners = new ArrayList<>(); + mHandle = ((WidgetManagerDelegate)aContext).newWidgetHandle(); mWidgetPlacement = new WidgetPlacement(aContext); initializeWidgetPlacement(mWidgetPlacement); - handleResizeEvent(SettingsStore.getInstance(getContext()).getBrowserWorldWidth(), - SettingsStore.getInstance(getContext()).getBrowserWorldHeight()); - mSaveResizeChanges = true; + mTopBar = new TopBarWidget(aContext); + mTopBar.attachToWindow(this); + mLastMouseClickPos = new Point(0, 0); + + setFocusable(true); + + TelemetryWrapper.openWindowEvent(mWindowId); } @Override protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { - Context context = getContext(); - aPlacement.width = SettingsStore.getInstance(getContext()).getWindowWidth() + mBorderWidth * 2; + int windowWidth = SettingsStore.getInstance(getContext()).getWindowWidth(); + aPlacement.width = windowWidth + mBorderWidth * 2; aPlacement.height = SettingsStore.getInstance(getContext()).getWindowHeight() + mBorderWidth * 2; + aPlacement.worldWidth = WidgetPlacement.floatDimension(getContext(), R.dimen.window_world_width) * + (float)windowWidth / (float)SettingsStore.WINDOW_WIDTH_DEFAULT; aPlacement.density = 1.0f; - aPlacement.translationX = 0.0f; - aPlacement.translationY = WidgetPlacement.unitFromMeters(context, R.dimen.window_world_y); - aPlacement.translationZ = WidgetPlacement.unitFromMeters(context, R.dimen.window_world_z); - aPlacement.anchorX = 0.5f; - aPlacement.anchorY = 0.0f; aPlacement.visible = true; aPlacement.cylinder = true; aPlacement.textureScale = 1.0f; + // Check Windows.placeWindow method for remaining placement set-up } @Override @@ -118,6 +148,8 @@ public void show(@ShowFlags int aShowFlags) { } else { clearFocus(); } + + mSessionStack.setActive(true); } @Override @@ -129,33 +161,56 @@ public void hide(@HideFlags int aHideFlag) { mWidgetManager.updateWidget(this); clearFocus(); + + mSessionStack.setActive(false); } @Override protected void onDismiss() { - if (mView != null) { - if (SessionStore.get().canGoBack()) { - SessionStore.get().goBack(); + if (isBookmarksVisible()) { + switchBookmarks(); - } else if (SessionStore.get().canUnstackSession()) { - SessionStore.get().unstackSession(); + } else { + SessionStack activeStore = SessionStore.get().getSessionStack(mWindowId); + if (activeStore.canGoBack()) { + activeStore.goBack(); } + } + } + + public void close() { + TelemetryWrapper.closeWindowEvent(mWindowId); - unsetView(mView); + releaseWidget(); + mBookmarksView.onDestroy(); + SessionStore.get().destroySessionStack(mWindowId); + } + + public void loadHomeIfNotRestored() { + if (!mIsRestored) + loadHome(); + } + + public void loadHome() { + if (mSessionStack.isPrivateMode()) { + InternalPages.PageResources pageResources = InternalPages.PageResources.create(R.raw.private_mode, R.raw.private_style); + mSessionStack.getCurrentSession().loadData(InternalPages.createAboutPage(getContext(), pageResources), "text/html"); + } else { + mSessionStack.loadUri(SettingsStore.getInstance(getContext()).getHomepage()); } } - public void setBookmarksView(BookmarksView view) { - mBookmarksView = view; + protected void setRestored(boolean restored) { + mIsRestored = restored; } - public void setView(View view) { + private void setView(View view) { pauseCompositor(); mView = view; removeView(view); mView.setVisibility(VISIBLE); addView(mView); - mWidgetPlacement.density = getContext().getResources().getDisplayMetrics().density; + mWidgetPlacement.density = getContext().getResources().getDisplayMetrics().density; if (mTexture != null && mSurface != null && mRenderer == null) { // Create the UI Renderer for the current surface. // Surface must be released when switching back to WebView surface or the browser @@ -169,7 +224,7 @@ public void setView(View view) { postInvalidate(); } - public void unsetView(View view) { + private void unsetView(View view) { if (mView != null && mView == view) { mView = null; removeView(view); @@ -191,6 +246,39 @@ public void unsetView(View view) { } } + public boolean isBookmarksVisible() { + return (mView != null); + } + + public void addBookmarksListener(@NonNull BookmarkListener listener) { + mBookmarksListeners.add(listener); + } + + public void removeBookmarksListener(@NonNull BookmarkListener listener) { + mBookmarksListeners.remove(listener); + } + + public void switchBookmarks() { + if (mView == null) { + setView(mBookmarksView); + for (BookmarkListener listener : mBookmarksListeners) + listener.onBookmarksShown(this); + + } else { + unsetView(mBookmarksView); + for (BookmarkListener listener : mBookmarksListeners) + listener.onBookmarksHidden(this); + } + } + + public void hideBookmarks() { + if (mView != null) { + unsetView(mBookmarksView); + for (BookmarkListener listener : mBookmarksListeners) + listener.onBookmarksHidden(this); + } + } + public void pauseCompositor() { if (mDisplay == null) { return; @@ -243,16 +331,24 @@ public void disableVRVideoMode() { mWidgetManager.updateWidget(this); } - @Override - public void resizeByMultiplier(float aspect, float multiplier) { - float worldWidth = WidgetPlacement.floatDimension(getContext(), R.dimen.window_world_width); - float worldHeight = worldWidth / aspect; - float area = worldWidth * worldHeight * multiplier; + public void setWindowPlacement(@NonNull Windows.WindowPlacement aPlacement) { + if (mActive) + TelemetryWrapper.activePlacementEvent(mWindowPlacement.getValue(), false); - float targetWidth = (float) Math.sqrt(area * aspect); - float targetHeight = (float) Math.sqrt(area / aspect); + mWindowPlacement = aPlacement; - handleResizeEvent(targetWidth, targetHeight); + if (mActive) + TelemetryWrapper.activePlacementEvent(mWindowPlacement.getValue(), true); + } + + public @NonNull Windows.WindowPlacement getWindowPlacement() { + return mWindowPlacement; + } + + @Override + public void resizeByMultiplier(float aspect, float multiplier) { + Pair targetSize = getSizeForScale(multiplier, aspect); + handleResizeEvent(targetSize.first, targetSize.second); } public float getCurrentScale() { @@ -273,8 +369,26 @@ public int getBorderWidth() { return mBorderWidth; } - public void setSaveResizeChanges(boolean aSave) { - mSaveResizeChanges = aSave; + public void setActiveWindow(boolean active) { + mActive = active; + if (active) { + SessionStore.get().setActiveStore(mWindowId); + mSessionId = mSessionStack.getCurrentSessionId(); + GeckoSession session = mSessionStack.getSession(mSessionId); + if (session != null) { + session.getTextInput().setView(this); + } + } + + TelemetryWrapper.activePlacementEvent(mWindowPlacement.getValue(), mActive); + } + + public SessionStack getSessionStack() { + return mSessionStack; + } + + public TopBarWidget getTopBar() { + return mTopBar; } @Override @@ -283,7 +397,7 @@ public void setSurfaceTexture(SurfaceTexture aTexture, final int aWidth, final i super.setSurfaceTexture(aTexture, aWidth, aHeight); } else { - GeckoSession session = SessionStore.get().getSession(mSessionId); + GeckoSession session = mSessionStack.getSession(mSessionId); if (session == null) { return; } @@ -311,7 +425,7 @@ public void setSurface(Surface aSurface, final int aWidth, final int aHeight, Ru super.setSurface(aSurface, aWidth, aHeight, aFirstDrawCallback); } else { - GeckoSession session = SessionStore.get().getSession(mSessionId); + GeckoSession session = mSessionStack.getSession(mSessionId); if (session == null) { return; } @@ -333,7 +447,8 @@ public void setSurface(Surface aSurface, final int aWidth, final int aHeight, Ru } private void callSurfaceChanged() { - mDisplay.surfaceChanged(mSurface, mBorderWidth, mBorderWidth, mWidth - mBorderWidth * 2, mHeight - mBorderWidth * 2); + if (mDisplay != null) + mDisplay.surfaceChanged(mSurface, mBorderWidth, mBorderWidth, mWidth - mBorderWidth * 2, mHeight - mBorderWidth * 2); } @Override @@ -365,15 +480,17 @@ public WidgetPlacement getPlacement() { @Override public void handleTouchEvent(MotionEvent aEvent) { + mLastMouseClickPos = new Point((int)aEvent.getX(), (int)aEvent.getY()); + if (mView != null) { super.handleTouchEvent(aEvent); } else { - if (aEvent.getActionMasked() == MotionEvent.ACTION_UP) { + if (aEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { requestFocus(); requestFocusFromTouch(); } - GeckoSession session = SessionStore.get().getSession(mSessionId); + GeckoSession session = mSessionStack.getSession(mSessionId); if (session == null) { return; } @@ -387,7 +504,8 @@ public void handleHoverEvent(MotionEvent aEvent) { super.handleHoverEvent(aEvent); } else { - GeckoSession session = SessionStore.get().getSession(mSessionId); + SessionStack activeStore = SessionStore.get().getActiveStore(); + GeckoSession session = activeStore.getSession(mSessionId); if (session == null) { return; } @@ -397,27 +515,24 @@ public void handleHoverEvent(MotionEvent aEvent) { @Override public void handleResizeEvent(float aWorldWidth, float aWorldHeight) { - float worldWidth = WidgetPlacement.floatDimension(getContext(), R.dimen.window_world_width); - int defaultWidth = SettingsStore.getInstance(getContext()).getWindowWidth(); - int defaultHeight = SettingsStore.getInstance(getContext()).getWindowHeight(); - - float aspect = (float) defaultWidth / (float) defaultHeight; - float worldHeight = worldWidth / aspect; - mWidgetPlacement.width = (int) ((aWorldWidth * defaultWidth) / worldWidth) + mBorderWidth * 2; - mWidgetPlacement.height = (int) ((aWorldHeight * defaultHeight) / worldHeight) + mBorderWidth * 2; + int width = getWindowWidth(aWorldWidth); + float aspect = aWorldWidth / aWorldHeight; + int height = (int) Math.floor((float)width / aspect); + mWidgetPlacement.width = width + mBorderWidth * 2; + mWidgetPlacement.height = height + mBorderWidth * 2; mWidgetPlacement.worldWidth = aWorldWidth; mWidgetManager.updateWidget(this); - if (mSaveResizeChanges) { - saveCurrentSize(); - } + mWidgetManager.updateVisibleWidgets(); } @Override public void releaseWidget() { - SessionStore.get().removeSessionChangeListener(this); - SessionStore.get().removePromptListener(this); - SessionStore.get().removeContentListener(this); - GeckoSession session = SessionStore.get().getSession(mSessionId); + mSessionStack.removeSessionChangeListener(this); + mSessionStack.removePromptListener(this); + mSessionStack.removeContentListener(this); + mSessionStack.removeVideoAvailabilityListener(this); + mSessionStack.removeNavigationListener(this); + GeckoSession session = mSessionStack.getSession(mSessionId); if (session == null) { return; } @@ -464,16 +579,7 @@ public void draw(Canvas aCanvas) { } } - // SessionStore.GeckoSessionChange - @Override - public void onNewSession(GeckoSession aSession, int aId) { - - } - - @Override - public void onRemoveSession(GeckoSession aSession, int aId) { - - } + // SessionStack.GeckoSessionChange @Override public void onCurrentSessionChange(GeckoSession aSession, int aId) { @@ -483,7 +589,7 @@ public void onCurrentSessionChange(GeckoSession aSession, int aId) { return; } - GeckoSession oldSession = SessionStore.get().getSession(mSessionId); + GeckoSession oldSession = mSessionStack.getSession(mSessionId); if (oldSession != null && mDisplay != null) { Log.d(LOGTAG, "Detach from previous session: " + mSessionId); oldSession.getTextInput().setView(null); @@ -511,7 +617,7 @@ public void onCurrentSessionChange(GeckoSession aSession, int aId) { @Override public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { Log.d(LOGTAG, "BrowserWidget onCreateInputConnection"); - GeckoSession session = SessionStore.get().getSession(mSessionId); + GeckoSession session = mSessionStack.getSession(mSessionId); if (session == null) { return null; } @@ -520,7 +626,8 @@ public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { @Override public boolean onCheckIsTextEditor() { - return SessionStore.get().isInputActive(mSessionId); + SessionStack sessionStack = SessionStore.get().getSessionStack(mWindowId); + return sessionStack.isInputActive(mSessionId); } @@ -529,7 +636,7 @@ public boolean onKeyPreIme(int aKeyCode, KeyEvent aEvent) { if (super.onKeyPreIme(aKeyCode, aEvent)) { return true; } - GeckoSession session = SessionStore.get().getSession(mSessionId); + GeckoSession session = mSessionStack.getSession(mSessionId); return (session != null) && session.getTextInput().onKeyPreIme(aKeyCode, aEvent); } @@ -538,7 +645,7 @@ public boolean onKeyUp(int aKeyCode, KeyEvent aEvent) { if (super.onKeyUp(aKeyCode, aEvent)) { return true; } - GeckoSession session = SessionStore.get().getSession(mSessionId); + GeckoSession session = mSessionStack.getSession(mSessionId); return (session != null) && session.getTextInput().onKeyUp(aKeyCode, aEvent); } @@ -547,7 +654,7 @@ public boolean onKeyDown(int aKeyCode, KeyEvent aEvent) { if (super.onKeyDown(aKeyCode, aEvent)) { return true; } - GeckoSession session = SessionStore.get().getSession(mSessionId); + GeckoSession session = mSessionStack.getSession(mSessionId); return (session != null) && session.getTextInput().onKeyDown(aKeyCode, aEvent); } @@ -556,7 +663,7 @@ public boolean onKeyLongPress(int aKeyCode, KeyEvent aEvent) { if (super.onKeyLongPress(aKeyCode, aEvent)) { return true; } - GeckoSession session = SessionStore.get().getSession(mSessionId); + GeckoSession session = mSessionStack.getSession(mSessionId); return (session != null) && session.getTextInput().onKeyLongPress(aKeyCode, aEvent); } @@ -565,7 +672,7 @@ public boolean onKeyMultiple(int aKeyCode, int repeatCount, KeyEvent aEvent) { if (super.onKeyMultiple(aKeyCode, repeatCount, aEvent)) { return true; } - GeckoSession session = SessionStore.get().getSession(mSessionId); + GeckoSession session = mSessionStack.getSession(mSessionId); return (session != null) && session.getTextInput().onKeyMultiple(aKeyCode, repeatCount, aEvent); } @@ -577,13 +684,13 @@ protected void onFocusChanged(boolean aGainFocus, int aDirection, Rect aPrevious @Override public boolean onTouchEvent(MotionEvent aEvent) { - GeckoSession session = SessionStore.get().getSession(mSessionId); + GeckoSession session = mSessionStack.getSession(mSessionId); return (session != null) && session.getPanZoomController().onTouchEvent(aEvent); } @Override public boolean onGenericMotionEvent(MotionEvent aEvent) { - GeckoSession session = SessionStore.get().getSession(mSessionId); + GeckoSession session = mSessionStack.getSession(mSessionId); return (session != null) && session.getPanZoomController().onMotionEvent(aEvent); } @@ -602,12 +709,6 @@ public void setNoInternetToastVisible(boolean aVisible) { } } - public void saveCurrentSize() { - final float aspect = (float)mWidgetPlacement.width / (float)mWidgetPlacement.height; - SettingsStore.getInstance(getContext()).setBrowserWorldWidth(mWidgetPlacement.worldWidth); - SettingsStore.getInstance(getContext()).setBrowserWorldHeight(mWidgetPlacement.worldWidth / aspect); - } - public void showAlert(String title, @NonNull String msg, @NonNull AlertCallback callback) { mAlertPrompt = new AlertPromptWidget(getContext()); mAlertPrompt.mWidgetPlacement.parentHandle = getHandle(); @@ -627,6 +728,50 @@ public void showButtonPrompt(String title, @NonNull String msg, @NonNull String[ mConfirmPrompt.show(REQUEST_FOCUS); } + public void showMaxWindowsDialog(int maxDialogs) { + mMaxWindowsDialog = new MaxWindowsWidget(getContext()); + mMaxWindowsDialog.mWidgetPlacement.parentHandle = getHandle(); + mMaxWindowsDialog.setMessage(getContext().getString(R.string.max_windows_message, String.valueOf(maxDialogs))); + mMaxWindowsDialog.show(REQUEST_FOCUS); + } + + public void setMaxWindowScale(float aScale) { + if (mMaxWindowScale != aScale) { + mMaxWindowScale = aScale; + + Pair maxSize = getSizeForScale(aScale); + + if (mWidgetPlacement.worldWidth > maxSize.first) { + float currentAspect = (float) mWidgetPlacement.width / (float) mWidgetPlacement.height; + mWidgetPlacement.worldWidth = maxSize.first; + mWidgetPlacement.width = getWindowWidth(maxSize.first); + mWidgetPlacement.height = (int) Math.ceil((float)mWidgetPlacement.width / currentAspect); + } + } + } + + public float getMaxWindowScale() { + return mMaxWindowScale; + } + + public @NonNull Pair getSizeForScale(float aScale) { + return getSizeForScale(aScale, SettingsStore.getInstance(getContext()).getWindowAspect()); + } + + public @NonNull Pair getSizeForScale(float aScale, float aAspect) { + float worldWidth = WidgetPlacement.floatDimension(getContext(), R.dimen.window_world_width) * + (float)SettingsStore.getInstance(getContext()).getWindowWidth() / (float)SettingsStore.WINDOW_WIDTH_DEFAULT; + float worldHeight = worldWidth / aAspect; + float area = worldWidth * worldHeight * aScale; + float targetWidth = (float) Math.sqrt(area * aAspect); + float targetHeight = targetWidth / aAspect; + return Pair.create(targetWidth, targetHeight); + } + + private int getWindowWidth(float aWorldWidth) { + return (int) Math.floor(SettingsStore.WINDOW_WIDTH_DEFAULT * aWorldWidth / WidgetPlacement.floatDimension(getContext(), R.dimen.window_world_width)); + } + // PromptDelegate @Override @@ -683,110 +828,63 @@ public void onChoicePrompt(GeckoSession session, String title, String msg, int t mChoicePrompt.show(REQUEST_FOCUS); } - @Override - public void onColorPrompt(GeckoSession session, String title, String value, TextCallback callback) { - - } - - @Override - public void onDateTimePrompt(GeckoSession session, String title, int type, String value, String min, String max, TextCallback callback) { - - } - - @Override - public void onFilePrompt(GeckoSession session, String title, int type, String[] mimeTypes, FileCallback callback) { - - } - @Override public GeckoResult onPopupRequest(final GeckoSession session, final String targetUri) { return GeckoResult.fromValue(AllowOrDeny.ALLOW); } - // BookmarkListener - - @Override - public void onBookmarksShown() { - - } - - @Override - public void onBookmarksHidden() { - unsetView(mBookmarksView); - } - - // TrayListener + // GeckoSession.ContentDelegate @Override - public void onBookmarksClicked() { - if (mBookmarksView.getVisibility() == View.VISIBLE) { - unsetView(mBookmarksView); - SessionStore.get().unstackSession(); - - } else { - int sessionId; - if (SessionStore.get().isCurrentSessionPrivate()) { - sessionId = SessionStore.get().createSession(true); - - } else { - sessionId = SessionStore.get().createSession(); - } - SessionStore.get().stackSession(sessionId); + public void onContextMenu(GeckoSession session, int screenX, int screenY, ContextElement element) { + TelemetryWrapper.longPressContextMenuEvent(); - setView(mBookmarksView); + if (mContextMenu != null) { + mContextMenu.hide(REMOVE_WIDGET); } - } - - @Override - public void onPrivateBrowsingClicked() { - if (mBookmarksView.getVisibility() == VISIBLE) { - SessionStore.get().unstackSession(); - } - } - - // GeckoSession.ContentDelegate - @Override - public void onTitleChange(GeckoSession session, String title) { + mContextMenu = new ContextMenuWidget(getContext()); + mContextMenu.mWidgetPlacement.parentHandle = getHandle(); + mContextMenu.setContextElement(mLastMouseClickPos, element); + mContextMenu.show(REQUEST_FOCUS); } @Override - public void onFocusRequest(GeckoSession session) { - + public void onFirstComposite(GeckoSession session) { + if (mFirstDrawCallback != null) { + // Post this call because running it synchronously can cause a deadlock if the runnable + // resizes the widget and calls surfaceChanged. See https://github.com/MozillaReality/FirefoxReality/issues/1459. + ThreadUtils.postToUiThread(mFirstDrawCallback); + mFirstDrawCallback = null; + } } - @Override - public void onCloseRequest(GeckoSession session) { - - } + // VideoAvailabilityListener @Override - public void onFullScreen(GeckoSession session, boolean fullScreen) { - + public void onVideoAvailabilityChanged(boolean aVideosAvailable) { + mWidgetManager.setCPULevel(aVideosAvailable ? + WidgetManagerDelegate.CPU_LEVEL_HIGH : + WidgetManagerDelegate.CPU_LEVEL_NORMAL); } - @Override - public void onContextMenu(GeckoSession session, int screenX, int screenY, ContextElement element) { - - } + // GeckoSession.NavigationDelegate @Override - public void onExternalResponse(GeckoSession session, GeckoSession.WebResponseInfo response) { - + public void onLocationChange(@NonNull GeckoSession session, @Nullable String url) { + if (isBookmarksVisible()) + switchBookmarks(); } + @Nullable @Override - public void onCrash(GeckoSession session) { + public GeckoResult onLoadRequest(@NonNull GeckoSession session, @NonNull LoadRequest request) { + if (request.isRedirect) + SessionStore.get().getHistoryStore().addHistory(request.uri, VisitType.EMBED); + else if (request.triggerUri != null) + SessionStore.get().getHistoryStore().addHistory(request.uri, VisitType.LINK); + return GeckoResult.ALLOW; } - @Override - public void onFirstComposite(GeckoSession session) { - if (mFirstDrawCallback != null) { - // Post this call because running it synchronously can cause a deadlock if the runnable - // resizes the widget and calls surfaceChanged. See https://github.com/MozillaReality/FirefoxReality/issues/1459. - ThreadUtils.postToUiThread(mFirstDrawCallback); - mFirstDrawCallback = null; - } - } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java new file mode 100644 index 000000000..cbbcb7066 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java @@ -0,0 +1,719 @@ +package org.mozilla.vrbrowser.ui.widgets; + +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import org.mozilla.geckoview.GeckoSession; +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.engine.SessionStack; +import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.Type; +import java.util.ArrayList; + +public class Windows implements TrayListener, TopBarWidget.Delegate, GeckoSession.ContentDelegate { + + private static final String WINDOWS_SAVE_FILENAME = "windows_state.json"; + + class WindowState { + WindowPlacement placement; + SessionStack sessionStack; + int currentSessionId; + int textureWidth; + int textureHeight; + float worldWidth; + + public void load(WindowWidget aWindow) { + placement = aWindow.getWindowPlacement(); + sessionStack = aWindow.getSessionStack(); + currentSessionId = aWindow.getSessionStack().getCurrentSessionId(); + textureWidth = aWindow.getPlacement().width; + textureHeight = aWindow.getPlacement().height; + worldWidth = aWindow.getPlacement().worldWidth; + } + } + + class WindowsState { + WindowPlacement focusedWindowPlacement = WindowPlacement.FRONT; + ArrayList regularWindowsState = new ArrayList<>(); + ArrayList privateWindowsState = new ArrayList<>(); + boolean privateMode = false; + } + + private Context mContext; + private WidgetManagerDelegate mWidgetManager; + private Delegate mDelegate; + private ArrayList mRegularWindows; + private ArrayList mPrivateWindows; + private WindowWidget mFocusedWindow; + private static int sIndex; + private boolean mPrivateMode = false; + public static final int MAX_WINDOWS = 3; + private WindowWidget mFullscreenWindow; + private WindowPlacement mPrevWindowPlacement; + + public enum WindowPlacement{ + FRONT(0), + LEFT(1), + RIGHT(2); + + private final int value; + + WindowPlacement(final int aValue) { + value = aValue; + } + + public int getValue() { return value; } + } + + public interface Delegate { + void onFocusedWindowChanged(@NonNull WindowWidget aFocusedWindow, @Nullable WindowWidget aPrevFocusedWindow); + } + + public Windows(Context aContext) { + mContext = aContext; + mWidgetManager = (WidgetManagerDelegate) aContext; + mRegularWindows = new ArrayList<>(); + mPrivateWindows = new ArrayList<>(); + + restoreWindows(); + } + + private void saveState() { + File file = new File(mContext.getFilesDir(), WINDOWS_SAVE_FILENAME); + try (Writer writer = new FileWriter(file)) { + WindowsState state = new WindowsState(); + state.privateMode = mPrivateMode; + state.focusedWindowPlacement = mFocusedWindow.getWindowPlacement(); + state.regularWindowsState = new ArrayList<>(); + for (WindowWidget window : mRegularWindows) { + WindowState windowState = new WindowState(); + windowState.load(window); + state.regularWindowsState.add(windowState); + } + for (WindowWidget window : mPrivateWindows) { + WindowState windowState = new WindowState(); + windowState.load(window); + state.privateWindowsState.add(windowState); + } + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + gson.toJson(state, writer); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + private WindowsState restoreState() { + WindowsState restored = null; + + File file = new File(mContext.getFilesDir(), WINDOWS_SAVE_FILENAME); + try (Reader reader = new FileReader(file)) { + Gson gson = new GsonBuilder().create(); + Type type = new TypeToken() {}.getType(); + restored = gson.fromJson(reader, type); + + } catch (IOException e) { + Log.w(getClass().getCanonicalName(), "Error restoring persistent windows state", e); + + } finally { + boolean deleted = file.delete(); + if (deleted) + Log.d(getClass().getCanonicalName(), "Persistent windows state successfully restored"); + else + Log.d(getClass().getCanonicalName(), "Persistent window state couldn't be deleted"); + } + + return restored; + } + + public void setDelegate(Delegate aDelegate) { + mDelegate = aDelegate; + } + + public WindowWidget getFocusedWindow() { + if (mFullscreenWindow != null) { + return mFullscreenWindow; + } + return mFocusedWindow; + } + + public WindowWidget addWindow() { + if (getCurrentWindows().size() >= MAX_WINDOWS) { + showMaxWindowsMessage(); + return null; + } + + if (mFullscreenWindow != null) { + mFullscreenWindow.getSessionStack().exitFullScreen(); + onFullScreen(mFullscreenWindow.getSessionStack().getCurrentSession(), false); + } + + WindowWidget frontWindow = getFrontWindow(); + WindowWidget leftWindow = getLeftWindow(); + WindowWidget rightWindow = getRightWindow(); + + WindowWidget newWindow = createWindow(); + WindowWidget focusedWindow = getFocusedWindow(); + + if (frontWindow == null) { + // First window + placeWindow(newWindow, WindowPlacement.FRONT); + } else if (leftWindow == null && rightWindow == null) { + // Opening a new window from one window + placeWindow(newWindow, WindowPlacement.FRONT); + placeWindow(frontWindow, WindowPlacement.LEFT); + } else if (leftWindow != null && focusedWindow == leftWindow) { + // Opening a new window from left window + placeWindow(newWindow, WindowPlacement.FRONT); + placeWindow(frontWindow, WindowPlacement.RIGHT); + } else if (leftWindow != null && focusedWindow == frontWindow) { + // Opening a new window from front window + placeWindow(newWindow, WindowPlacement.RIGHT); + } else if (rightWindow != null && focusedWindow == rightWindow) { + // Opening a new window from right window + placeWindow(newWindow, WindowPlacement.FRONT); + placeWindow(frontWindow, WindowPlacement.LEFT); + } else if (rightWindow != null && focusedWindow == frontWindow) { + // Opening a new window from right window + placeWindow(newWindow, WindowPlacement.LEFT); + } + + updateMaxWindowScales(); + mWidgetManager.addWidget(newWindow); + focusWindow(newWindow); + updateViews(); + return newWindow; + } + + private WindowWidget addWindow(WindowState aState) { + if (getCurrentWindows().size() >= MAX_WINDOWS) { + showMaxWindowsMessage(); + return null; + } + + WindowWidget newWindow = createWindow(); + newWindow.getPlacement().width = aState.textureWidth; + newWindow.getPlacement().height = aState.textureHeight; + newWindow.getPlacement().worldWidth = aState.worldWidth; + newWindow.setRestored(true); + placeWindow(newWindow, aState.placement); + + mWidgetManager.addWidget(newWindow); + return newWindow; + } + + public void closeWindow(@NonNull WindowWidget aWindow) { + WindowWidget frontWindow = getFrontWindow(); + WindowWidget leftWindow = getLeftWindow(); + WindowWidget rightWindow = getRightWindow(); + + aWindow.hideBookmarks(); + + if (leftWindow == aWindow) { + removeWindow(leftWindow); + if (mFocusedWindow == leftWindow) { + focusWindow(frontWindow); + } + } else if (rightWindow == aWindow) { + removeWindow(rightWindow); + if (mFocusedWindow == rightWindow) { + focusWindow(frontWindow); + } + } else if (frontWindow == aWindow) { + removeWindow(frontWindow); + if (rightWindow != null) { + placeWindow(rightWindow, WindowPlacement.FRONT); + } else if (leftWindow != null) { + placeWindow(leftWindow, WindowPlacement.FRONT); + } + + if (mFocusedWindow == frontWindow && !getCurrentWindows().isEmpty()) { + focusWindow(getFrontWindow()); + } + + } + + boolean empty = getCurrentWindows().isEmpty(); + if (empty && isInPrivateMode()) { + // Exit private mode if the only window is closed. + exitPrivateMode(); + } else if (empty) { + // Ensure that there is at least one window. + WindowWidget window = addWindow(); + if (window != null) + window.loadHome(); + } + + updateViews(); + } + + public void moveWindowRight(@NonNull WindowWidget aWindow) { + WindowWidget frontWindow = getFrontWindow(); + WindowWidget leftWindow = getLeftWindow(); + WindowWidget rightWindow = getRightWindow(); + + if (aWindow == leftWindow) { + placeWindow(leftWindow, WindowPlacement.FRONT); + placeWindow(frontWindow, WindowPlacement.LEFT); + } else if (aWindow == frontWindow) { + if (rightWindow != null) { + placeWindow(rightWindow, WindowPlacement.FRONT); + } else if (leftWindow != null) { + placeWindow(leftWindow, WindowPlacement.FRONT); + } + placeWindow(frontWindow, WindowPlacement.RIGHT); + } + updateViews(); + } + + public void moveWindowLeft(@NonNull WindowWidget aWindow) { + WindowWidget frontWindow = getFrontWindow(); + WindowWidget leftWindow = getLeftWindow(); + WindowWidget rightWindow = getRightWindow(); + + if (aWindow == rightWindow) { + placeWindow(rightWindow, WindowPlacement.FRONT); + placeWindow(frontWindow, WindowPlacement.RIGHT); + } else if (aWindow == frontWindow) { + if (leftWindow != null) { + placeWindow(leftWindow, WindowPlacement.FRONT); + } else if (rightWindow != null) { + placeWindow(rightWindow, WindowPlacement.FRONT); + } + placeWindow(frontWindow, WindowPlacement.LEFT); + } + updateViews(); + } + + public void focusWindow(@NonNull WindowWidget aWindow) { + if (aWindow != mFocusedWindow) { + WindowWidget prev = mFocusedWindow; + mFocusedWindow = aWindow; + if (prev != null) + prev.setActiveWindow(false); + mFocusedWindow.setActiveWindow(true); + if (mDelegate != null) { + mDelegate.onFocusedWindowChanged(mFocusedWindow, prev); + } + } + } + + public void pauseCompositor() { + for (WindowWidget window: mRegularWindows) { + window.pauseCompositor(); + } + for (WindowWidget window: mPrivateWindows) { + window.pauseCompositor(); + } + } + + public void resumeCompositor() { + for (WindowWidget window: mRegularWindows) { + window.resumeCompositor(); + } + for (WindowWidget window: mPrivateWindows) { + window.resumeCompositor(); + } + } + + public void onStop() { + saveState(); + } + + public void onDestroy() { + for (WindowWidget window: mRegularWindows) { + window.close(); + } + for (WindowWidget window: mPrivateWindows) { + window.close(); + } + } + + public boolean isInPrivateMode() { + return mPrivateMode; + } + + + public void enterPrivateMode() { + if (mPrivateMode) { + return; + } + mPrivateMode = true; + for (WindowWidget window: mRegularWindows) { + setWindowVisible(window, false); + } + for (WindowWidget window: mPrivateWindows) { + setWindowVisible(window, true); + } + + if (mPrivateWindows.size() == 0) { + WindowWidget window = addWindow(); + if (window != null) + window.loadHome(); + + } else { + focusWindow(getFrontWindow()); + } + mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); + } + + public void exitPrivateMode() { + if (!mPrivateMode) { + return; + } + mPrivateMode = false; + for (WindowWidget window: mRegularWindows) { + setWindowVisible(window, true); + } + for (WindowWidget window: mPrivateWindows) { + setWindowVisible(window, false); + } + focusWindow(getFrontWindow()); + mWidgetManager.popWorldBrightness(this); + } + + public boolean handleBack() { + if (mFocusedWindow == null) { + return false; + } + if (mFocusedWindow.getSessionStack().canGoBack()) { + mFocusedWindow.getSessionStack().goBack(); + return true; + } else if (isInPrivateMode()) { + exitPrivateMode(); + return true; + } + + return false; + } + + private void updateMaxWindowScales() { + float maxScale = 3; + if (mFullscreenWindow == null && getCurrentWindows().size() >= 3) { + maxScale = 1.5f; + } else if (mFullscreenWindow == null && getCurrentWindows().size() == 2) { + maxScale = 2.0f; + } + + for (WindowWidget window: getCurrentWindows()) { + window.setMaxWindowScale(maxScale); + } + } + + private void showMaxWindowsMessage() { + TelemetryWrapper.maxWindowsDialogEvent(); + mFocusedWindow.showMaxWindowsDialog(MAX_WINDOWS); + } + + private ArrayList getCurrentWindows() { + return mPrivateMode ? mPrivateWindows : mRegularWindows; + } + + private WindowWidget getWindowWithPlacement(WindowPlacement aPlacement) { + for (WindowWidget window: getCurrentWindows()) { + if (window.getWindowPlacement() == aPlacement) { + return window; + } + } + return null; + } + + private WindowWidget getFrontWindow() { + if (mFullscreenWindow != null) { + return mFullscreenWindow; + } + return getWindowWithPlacement(WindowPlacement.FRONT); + } + + private WindowWidget getLeftWindow() { + return getWindowWithPlacement(WindowPlacement.LEFT); + } + + private WindowWidget getRightWindow() { + return getWindowWithPlacement(WindowPlacement.RIGHT); + } + + private void restoreWindows() { + WindowsState windowsState = restoreState(); + if (windowsState != null) { + for (WindowState windowState : windowsState.regularWindowsState) { + WindowWidget window = addWindow(windowState); + window.getSessionStack().restore(windowState.sessionStack, windowState.currentSessionId); + } + mPrivateMode = true; + for (WindowState windowState : windowsState.privateWindowsState) { + WindowWidget window = addWindow(windowState); + window.getSessionStack().restore(windowState.sessionStack, windowState.currentSessionId); + } + mPrivateMode = false; + if (windowsState.privateMode) { + enterPrivateMode(); + } + + WindowWidget windowToFocus = getWindowWithPlacement(windowsState.focusedWindowPlacement); + if (windowToFocus == null) { + windowToFocus = getFrontWindow(); + if (windowToFocus == null && getCurrentWindows().size() > 0) { + windowToFocus = getCurrentWindows().get(0); + } + } + if (windowToFocus != null) { + focusWindow(windowToFocus); + } + + } + if (getCurrentWindows().size() == 0) { + WindowWidget window = addWindow(); + focusWindow(window); + } + updateMaxWindowScales(); + updateViews(); + } + + private void removeWindow(@NonNull WindowWidget aWindow) { + mWidgetManager.removeWidget(aWindow); + mRegularWindows.remove(aWindow); + mPrivateWindows.remove(aWindow); + aWindow.getTopBar().setVisible(false); + aWindow.getTopBar().setDelegate((TopBarWidget.Delegate) null); + aWindow.getSessionStack().removeContentListener(this); + aWindow.close(); + updateMaxWindowScales(); + + if (aWindow.getSessionStack().isPrivateMode()) + TelemetryWrapper.openWindowsEvent(mPrivateWindows.size()+1, mPrivateWindows.size(), true); + else + TelemetryWrapper.openWindowsEvent(mRegularWindows.size()+1, mRegularWindows.size(), false); + } + + private void setWindowVisible(@NonNull WindowWidget aWindow, boolean aVisible) { + aWindow.setVisible(aVisible); + aWindow.getTopBar().setVisible(aVisible); + } + + private void placeWindow(@NonNull WindowWidget aWindow, WindowPlacement aPosition) { + WidgetPlacement placement = aWindow.getPlacement(); + aWindow.setWindowPlacement(aPosition); + boolean curved = SettingsStore.getInstance(mContext).getCylinderDensity() > 0; + Log.e("VRB", "mortimer curved: " + curved); + switch (aPosition) { + case FRONT: + placement.anchorX = 0.5f; + placement.anchorY = 0.0f; + placement.rotation = 0; + placement.rotationAxisX = 0; + placement.rotationAxisY = 0; + placement.rotationAxisZ = 0; + placement.translationX = 0.0f; + placement.translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.window_world_y); + placement.translationZ = WidgetPlacement.unitFromMeters(mContext, R.dimen.window_world_z); + break; + case LEFT: + placement.anchorX = 1.0f; + placement.anchorY = 0.0f; + placement.parentAnchorX = 0.0f; + placement.parentAnchorY = 0.0f; + placement.rotationAxisX = 0; + placement.rotationAxisZ = 0; + if (curved) { + placement.rotationAxisY = 0; + placement.rotation = 0; + } else { + placement.rotationAxisY = 1.0f; + placement.rotation = (float) Math.toRadians(WidgetPlacement.floatDimension(mContext, R.dimen.multi_window_angle)); + } + placement.translationX = -WidgetPlacement.dpDimension(mContext, R.dimen.multi_window_padding); + placement.translationY = 0.0f; + placement.translationZ = 0.0f; + break; + case RIGHT: + placement.anchorX = 0.0f; + placement.anchorY = 0.0f; + placement.parentAnchorX = 1.0f; + placement.parentAnchorY = 0.0f; + placement.rotationAxisX = 0; + placement.rotationAxisZ = 0; + if (curved) { + placement.rotationAxisY = 0; + placement.rotation = 0; + } else { + placement.rotationAxisY = 1.0f; + placement.rotation = (float) Math.toRadians(-WidgetPlacement.floatDimension(mContext, R.dimen.multi_window_angle)); + } + + placement.translationX = WidgetPlacement.dpDimension(mContext, R.dimen.multi_window_padding); + placement.translationY = 0.0f; + placement.translationZ = 0.0f; + } + } + + private void updateViews() { + WindowWidget frontWindow = getFrontWindow(); + WindowWidget leftWindow = getLeftWindow(); + WindowWidget rightWindow = getRightWindow(); + // Make sure that left or right window have the correct parent + if (frontWindow != null && leftWindow != null) { + leftWindow.getPlacement().parentHandle = frontWindow.getHandle(); + } + if (frontWindow != null && rightWindow != null) { + rightWindow.getPlacement().parentHandle = frontWindow.getHandle(); + } + if (frontWindow != null) { + frontWindow.getPlacement().parentHandle = -1; + } + + updateTopBars(); + ArrayList windows = getCurrentWindows(); + // Sort windows so frontWindow is the first one. Required for proper native matrix updates. + windows.sort((o1, o2) -> o1 == frontWindow ? -1 : 0); + for (WindowWidget window: getCurrentWindows()) { + mWidgetManager.updateWidget(window); + mWidgetManager.updateWidget(window.getTopBar()); + } + } + + private void updateTopBars() { + ArrayList windows = getCurrentWindows(); + WindowWidget leftWindow = getLeftWindow(); + WindowWidget rightWindow = getRightWindow(); + boolean visible = mFullscreenWindow == null && (windows.size() > 1 || isInPrivateMode()); + for (WindowWidget window: windows) { + window.getTopBar().setVisible(visible); + if (visible) { + window.getTopBar().setMoveLeftButtonEnabled(window != leftWindow); + window.getTopBar().setMoveRightButtonEnabled(window != rightWindow); + } + } + if (isInPrivateMode() && mPrivateWindows.size() == 1) { + mFocusedWindow.getTopBar().setMoveLeftButtonEnabled(false); + mFocusedWindow.getTopBar().setMoveRightButtonEnabled(false); + } + } + + private WindowWidget createWindow() { + int newWindowId = sIndex++; + WindowWidget window = new WindowWidget(mContext, newWindowId, mPrivateMode); + getCurrentWindows().add(window); + window.getTopBar().setDelegate(this); + window.getSessionStack().addContentListener(this); + + if (mPrivateMode) + TelemetryWrapper.openWindowsEvent(mPrivateWindows.size()-1, mPrivateWindows.size(), true); + else + TelemetryWrapper.openWindowsEvent(mRegularWindows.size()-1, mRegularWindows.size(), false); + + return window; + } + + public void onCurvedModeChanged() { + for (WindowWidget window: getCurrentWindows()) { + placeWindow(window, window.getWindowPlacement()); + } + updateViews(); + } + + // Tray Listener + @Override + public void onBookmarksClicked() { + mFocusedWindow.switchBookmarks(); + } + + @Override + public void onPrivateBrowsingClicked() { + if (mPrivateMode) { + exitPrivateMode(); + } else { + enterPrivateMode(); + } + } + + @Override + public void onAddWindowClicked() { + WindowWidget window = addWindow(); + if (window != null) + window.loadHome(); + } + + // TopBarWidget Delegate + @Override + public void onCloseClicked(TopBarWidget aWidget) { + WindowWidget window = aWidget.getAttachedWindow(); + if (window != null) { + closeWindow(window); + } + } + + @Override + public void onMoveLeftClicked(TopBarWidget aWidget) { + WindowWidget window = aWidget.getAttachedWindow(); + if (window != null) { + TelemetryWrapper.windowsMoveEvent(); + + moveWindowLeft(window); + } + } + + @Override + public void onMoveRightClicked(TopBarWidget aWidget) { + WindowWidget window = aWidget.getAttachedWindow(); + if (window != null) { + TelemetryWrapper.windowsMoveEvent(); + + moveWindowRight(window); + } + } + + // Content delegate + @Override + public void onFullScreen(GeckoSession session, boolean aFullScreen) { + WindowWidget window = getWindowWithSession(session); + if (window == null) { + return; + } + + if (aFullScreen) { + mFullscreenWindow = window; + mPrevWindowPlacement = window.getWindowPlacement(); + placeWindow(window, WindowPlacement.FRONT); + focusWindow(window); + for (WindowWidget win: getCurrentWindows()) { + setWindowVisible(win, win == mFullscreenWindow); + } + updateMaxWindowScales(); + updateViews(); + } else if (mFullscreenWindow != null) { + placeWindow(mFullscreenWindow, mPrevWindowPlacement); + mFullscreenWindow = null; + for (WindowWidget win : getCurrentWindows()) { + setWindowVisible(win, true); + } + updateMaxWindowScales(); + updateViews(); + } + } + + private WindowWidget getWindowWithSession(GeckoSession aSession) { + for (WindowWidget window: getCurrentWindows()) { + if (window.getSessionStack().containsSession(aSession)) { + return window; + } + } + return null; + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ContextMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ContextMenuWidget.java new file mode 100644 index 000000000..9c66d1511 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ContextMenuWidget.java @@ -0,0 +1,184 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.vrbrowser.ui.widgets.dialogs; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.PointF; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewTreeObserver; + +import org.mozilla.geckoview.GeckoSession; +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.ui.callbacks.ContextMenuClickCallback; +import org.mozilla.vrbrowser.ui.views.ContextMenu; +import org.mozilla.vrbrowser.ui.widgets.UIWidget; +import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; +import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; +import org.mozilla.vrbrowser.utils.ViewUtils; + +public class ContextMenuWidget extends UIWidget implements WidgetManagerDelegate.FocusChangeListener { + + private GeckoSession.ContentDelegate.ContextElement mContextElement; + private ContextMenu mContextMenu; + private int mMaxHeight; + private Point mMousePos; + + public ContextMenuWidget(Context aContext) { + super(aContext); + + mContextMenu = new ContextMenu(aContext); + initialize(); + } + + public ContextMenuWidget(Context aContext, AttributeSet aAttrs) { + super(aContext, aAttrs); + + mContextMenu = new ContextMenu(aContext, aAttrs); + initialize(); + } + + public ContextMenuWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { + super(aContext, aAttrs, aDefStyle); + + mContextMenu = new ContextMenu(aContext, aAttrs, aDefStyle); + initialize(); + } + + private void initialize() { + addView(mContextMenu); + mContextMenu.setContextMenuClickCallback(mContextMenuClickCallback); + } + + @Override + protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { + aPlacement.visible = false; + aPlacement.width = WidgetPlacement.pixelDimension(getContext(), R.dimen.browser_width_pixels)/2; + mMaxHeight = WidgetPlacement.dpDimension(getContext(), R.dimen.prompt_height); + aPlacement.height = mMaxHeight; + aPlacement.parentAnchorX = 0.0f; + aPlacement.parentAnchorY = 1.0f; + aPlacement.anchorX = 0.5f; + aPlacement.anchorY = 0.5f; + aPlacement.opaque = false; + aPlacement.cylinder = true; + aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.context_menu_z_distance); + } + + @Override + public void show(@ShowFlags int aShowFlags) { + mWidgetManager.addFocusChangeListener(ContextMenuWidget.this); + + mContextMenu.measure(View.MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mWidgetPlacement.width = (int)(mContextMenu.getMeasuredWidth()/mWidgetPlacement.density); + mWidgetPlacement.height = (int)(mContextMenu.getMeasuredHeight()/mWidgetPlacement.density); + super.show(aShowFlags); + + ViewTreeObserver viewTreeObserver = mContextMenu.getViewTreeObserver(); + if (viewTreeObserver.isAlive()) { + viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + mContextMenu.getViewTreeObserver().removeOnGlobalLayoutListener(this); + PointF anchor = anchorForCurrentMousePosition(); + mWidgetPlacement.anchorX = anchor.x; + mWidgetPlacement.anchorY = anchor.y; + mWidgetPlacement.translationX = mMousePos.x * WidgetPlacement.worldToWindowRatio(getContext()); + mWidgetPlacement.translationY = -(mMousePos.y * WidgetPlacement.worldToWindowRatio(getContext())); + mWidgetPlacement.width = (int)(mContextMenu.getWidth()/mWidgetPlacement.density); + mWidgetPlacement.height = (int)(mContextMenu.getHeight()/mWidgetPlacement.density); + mWidgetManager.updateWidget(ContextMenuWidget.this); + } + }); + } + } + + @Override + public void hide(@HideFlags int aHideFlags) { + super.hide(aHideFlags); + + mWidgetManager.removeFocusChangeListener(this); + } + + @Override + protected void onDismiss() { + hide(REMOVE_WIDGET); + } + + private final ContextMenuClickCallback mContextMenuClickCallback = contextMenuNode -> { + mWidgetManager.openNewWindow(mContextElement.linkUri); + hide(REMOVE_WIDGET); + }; + + public void setContextElement(Point mousePos, GeckoSession.ContentDelegate.ContextElement element) { + mMousePos = mousePos; + mContextElement = element; + + switch (mContextElement.type) { + case GeckoSession.ContentDelegate.ContextElement.TYPE_AUDIO: + mContextMenu.createAudioContextMenu(); + break; + + case GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE: + mContextMenu.createImageContextMenu(); + break; + + case GeckoSession.ContentDelegate.ContextElement.TYPE_NONE: + mContextMenu.createLinkContextMenu(); + break; + + case GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO: + mContextMenu.createVideoContextMenu(); + break; + } + } + + private PointF anchorForCurrentMousePosition() { + float browserWindowWidth = SettingsStore.getInstance(getContext()).getWindowWidth(); + float browserWindowHeight = SettingsStore.getInstance(getContext()).getWindowHeight(); + float halfWidth = WidgetPlacement.convertPixelsToDp(getContext(), getWidth()); + float halfHeight = WidgetPlacement.convertPixelsToDp(getContext(), getHeight()); + if (mMousePos.x > (browserWindowWidth - halfWidth)) { + if (mMousePos.y < halfHeight) + // Top Right + return new PointF(1.0f, 1.0f); + else + // Middle/Bottom Right + return new PointF(1.0f, 0.0f); + + } else if (mMousePos.x < (browserWindowWidth + halfWidth)) { + if (mMousePos.y < halfHeight) + // Top Left + return new PointF(0.0f, 1.0f); + else + // Middle/Bottom Left + new PointF(0.0f, 0.0f); + + } else { + if (mMousePos.y < halfHeight) + // Top Middle + return new PointF(1.0f, 1.0f); + else if (mMousePos.y > (browserWindowHeight - halfHeight)) + // Bottom Middle + return new PointF(0.0f, 0.0f); + } + + return new PointF(0.0f, 0.0f); + } + + // WidgetManagerDelegate.FocusChangeListener + + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + if (!ViewUtils.isChildrenOf(mContextMenu, newFocus)) { + onDismiss(); + } + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/CrashDialogWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/CrashDialogWidget.java index 59f251e9c..abbba8a29 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/CrashDialogWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/CrashDialogWidget.java @@ -7,17 +7,14 @@ import android.content.Context; import android.util.AttributeSet; -import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.TextView; -import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; -import org.mozilla.vrbrowser.browser.SessionStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.browser.SettingsStore; -import org.mozilla.vrbrowser.ui.widgets.UIWidget; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; @@ -68,13 +65,7 @@ private void initialize(Context aContext) { mAudio.playSound(AudioEngine.Sound.CLICK); } - GeckoSession session = SessionStore.get().getCurrentSession(); - if (session == null) { - int sessionId = SessionStore.get().createSession(); - SessionStore.get().setCurrentSession(sessionId); - } - - SessionStore.get().loadUri(getContext().getString(R.string.crash_dialog_learn_more_url)); + SessionStore.get().getActiveStore().newSessionWithUrl(getContext().getString(R.string.crash_dialog_learn_more_url)); onDismiss(); }); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/MaxWindowsWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/MaxWindowsWidget.java new file mode 100644 index 000000000..cf3aec70f --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/MaxWindowsWidget.java @@ -0,0 +1,40 @@ +package org.mozilla.vrbrowser.ui.widgets.dialogs; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Button; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.ui.widgets.prompts.PromptWidget; + +public class MaxWindowsWidget extends PromptWidget { + + private Button mButton; + + public MaxWindowsWidget(Context aContext) { + super(aContext); + initialize(aContext); + } + + public MaxWindowsWidget(Context aContext, AttributeSet aAttrs) { + super(aContext, aAttrs); + initialize(aContext); + } + + public MaxWindowsWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { + super(aContext, aAttrs, aDefStyle); + initialize(aContext); + } + + protected void initialize(Context aContext) { + inflate(aContext, R.layout.prompt_max_windows, this); + + mLayout = findViewById(R.id.layout); + + mMessage = findViewById(R.id.alertMessage); + + mButton = findViewById(R.id.exitButton); + mButton.setOnClickListener(view -> onDismiss()); + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/UIDialog.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/UIDialog.java index dba843f6f..d8b1d30ed 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/UIDialog.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/UIDialog.java @@ -39,6 +39,7 @@ public boolean isDialog() { } // WidgetManagerDelegate.FocusChangeListener + @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { if (oldFocus == this && isVisible()) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/VoiceSearchWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/VoiceSearchWidget.java index b0d707019..68aac8488 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/VoiceSearchWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/VoiceSearchWidget.java @@ -46,9 +46,9 @@ public class VoiceSearchWidget extends UIDialog implements WidgetManagerDelegate private static int MIN_DB = 50; public interface VoiceSearchDelegate { - void OnVoiceSearchResult(String transcription, float confidance); - void OnVoiceSearchCanceled(); - void OnVoiceSearchError(); + default void OnVoiceSearchResult(String transcription, float confidance) {}; + default void OnVoiceSearchCanceled() {}; + default void OnVoiceSearchError() {}; } private MozillaSpeechService mMozillaSpeechService; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/ContentLanguageOptionsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/ContentLanguageOptionsView.java index 2d085de24..98ef21b75 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/ContentLanguageOptionsView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/ContentLanguageOptionsView.java @@ -14,8 +14,8 @@ import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.browser.SessionStore; import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.databinding.OptionsLanguageContentBinding; import org.mozilla.vrbrowser.ui.adapters.Language; import org.mozilla.vrbrowser.ui.adapters.LanguagesAdapter; @@ -57,7 +57,7 @@ private void initialize(Context aContext) { mDelegate.showView(new LanguageOptionsView(getContext(), mWidgetManager)); }); mBinding.headerLayout.setHelpClickListener(view -> { - SessionStore.get().loadUri(getResources().getString(R.string.sumo_language_content_url)); + SessionStore.get().getActiveStore().loadUri(getResources().getString(R.string.sumo_language_content_url)); mDelegate.exitWholeSettings(); }); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/DeveloperOptionsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/DeveloperOptionsView.java index d38e7c248..633e1d78a 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/DeveloperOptionsView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/DeveloperOptionsView.java @@ -10,8 +10,8 @@ import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; -import org.mozilla.vrbrowser.browser.SessionStore; import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.ui.views.UIButton; import org.mozilla.vrbrowser.ui.views.settings.ButtonSetting; import org.mozilla.vrbrowser.ui.views.settings.SwitchSetting; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/DisplayLanguageOptionsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/DisplayLanguageOptionsView.java index 8f579b43e..d57ee3598 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/DisplayLanguageOptionsView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/DisplayLanguageOptionsView.java @@ -9,7 +9,7 @@ import android.graphics.Point; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.browser.SessionStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.ui.views.settings.RadioGroupSetting; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; @@ -33,7 +33,7 @@ private void initialize(Context aContext) { mDelegate.showView(new LanguageOptionsView(getContext(), mWidgetManager)); }); header.setHelpClickListener(view -> { - SessionStore.get().loadUri(getResources().getString(R.string.sumo_language_display_url)); + SessionStore.get().getActiveStore().loadUri(getResources().getString(R.string.sumo_language_display_url)); mDelegate.exitWholeSettings(); }); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/DisplayOptionsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/DisplayOptionsView.java index 49f4f4fb3..b59d4e293 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/DisplayOptionsView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/DisplayOptionsView.java @@ -11,8 +11,8 @@ import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; -import org.mozilla.vrbrowser.browser.SessionStore; import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.ui.views.UIButton; import org.mozilla.vrbrowser.ui.views.settings.ButtonSetting; import org.mozilla.vrbrowser.ui.views.settings.DoubleEditSetting; @@ -478,7 +478,8 @@ private void setMaxWindowSize(int newMaxWindowWidth, int newMaxWindowHeight, boo SettingsStore.getInstance(getContext()).setMaxWindowHeight(newMaxWindowHeight); if (doApply) { - SessionStore.get().setMaxWindowSize(newMaxWindowWidth, newMaxWindowHeight); + SettingsStore.getInstance(getContext()).setMaxWindowWidth(newMaxWindowWidth); + SettingsStore.getInstance(getContext()).setMaxWindowHeight(newMaxWindowHeight); showRestartDialog(); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/EnvironmentOptionsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/EnvironmentOptionsView.java index 19151af49..60b2d5000 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/EnvironmentOptionsView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/EnvironmentOptionsView.java @@ -6,13 +6,11 @@ package org.mozilla.vrbrowser.ui.widgets.settings; import android.content.Context; -import android.view.View; -import android.widget.ScrollView; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; -import org.mozilla.vrbrowser.browser.SessionStore; import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.ui.views.UIButton; import org.mozilla.vrbrowser.ui.views.settings.ButtonSetting; import org.mozilla.vrbrowser.ui.views.settings.ImageRadioGroupSetting; @@ -57,7 +55,7 @@ private void initialize(Context aContext) { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } - SessionStore.get().loadUri(getContext().getString(R.string.environment_override_help_url)); + SessionStore.get().getActiveStore().loadUri(getContext().getString(R.string.environment_override_help_url)); exitWholeSettings(); }); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/PrivacyOptionsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/PrivacyOptionsView.java index 9d767e5b4..d9027560d 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/PrivacyOptionsView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/PrivacyOptionsView.java @@ -18,8 +18,8 @@ import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; -import org.mozilla.vrbrowser.browser.SessionStore; import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.ui.views.UIButton; import org.mozilla.vrbrowser.ui.views.settings.ButtonSetting; import org.mozilla.vrbrowser.ui.views.settings.SwitchSetting; @@ -67,13 +67,7 @@ private void initialize(Context aContext) { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } - GeckoSession session = SessionStore.get().getCurrentSession(); - if (session == null) { - int sessionId = SessionStore.get().createSession(); - SessionStore.get().setCurrentSession(sessionId); - } - - SessionStore.get().loadUri(getContext().getString(R.string.private_policy_url)); + SessionStore.get().getActiveStore().newSessionWithUrl(getContext().getString(R.string.private_policy_url)); exitWholeSettings(); }); @@ -84,7 +78,7 @@ private void initialize(Context aContext) { // TODO Enable/Disable DRM content playback }); mDrmContentPlaybackSwitch.setLinkClickListener((widget, url) -> { - SessionStore.get().loadUri(url); + SessionStore.get().getActiveStore().loadUri(url); exitWholeSettings(); }); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/SettingsWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/SettingsWidget.java index f38c6b67f..c3c76fbf8 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/SettingsWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/SettingsWidget.java @@ -20,11 +20,11 @@ import android.widget.LinearLayout; import android.widget.TextView; -import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.BuildConfig; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; -import org.mozilla.vrbrowser.browser.SessionStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; +import org.mozilla.vrbrowser.browser.engine.SessionStack; import org.mozilla.vrbrowser.ui.views.HoneycombButton; import org.mozilla.vrbrowser.ui.widgets.UIWidget; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; @@ -96,7 +96,7 @@ private void initialize(Context aContext) { LinearLayout reportIssue = findViewById(R.id.reportIssueLayout); reportIssue.setOnClickListener(v -> { - SessionStore.get().loadUri(getContext().getString(R.string.bug_report_url)); + SessionStore.get().getActiveStore().loadUri(getContext().getString(R.string.bug_report_url)); onDismiss(); }); @@ -217,13 +217,8 @@ private void onSettingsPrivacyClick() { } private void onSettingsReportClick() { - String url = SessionStore.get().getCurrentUri(); - - GeckoSession session = SessionStore.get().getCurrentSession(); - if (session == null) { - int sessionId = SessionStore.get().createSession(); - SessionStore.get().setCurrentSession(sessionId); - } + SessionStack sessionStack = SessionStore.get().getActiveStore(); + String url = sessionStack.getCurrentUri(); try { if (url == null) { @@ -231,9 +226,9 @@ private void onSettingsReportClick() { url = ""; } else if (url.startsWith("jar:") || url.startsWith("resource:") || url.startsWith("about:")) { url = ""; - } else if (SessionStore.get().isHomeUri(url)) { + } else if (sessionStack.isHomeUri(url)) { // Use the original URL (without any hash). - url = SessionStore.get().getHomeUri(); + url = sessionStack.getHomeUri(); } url = URLEncoder.encode(url, "UTF-8"); @@ -241,7 +236,8 @@ private void onSettingsReportClick() { } catch (UnsupportedEncodingException e) { Log.e(LOGTAG, "Cannot encode URL"); } - SessionStore.get().loadUri(getContext().getString(R.string.private_report_url, url)); + + sessionStack.newSessionWithUrl(getContext().getString(R.string.private_report_url, url)); hide(REMOVE_WIDGET); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/VoiceSearchLanguageOptionsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/VoiceSearchLanguageOptionsView.java index f6775e73d..1b068e97f 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/VoiceSearchLanguageOptionsView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/VoiceSearchLanguageOptionsView.java @@ -9,8 +9,7 @@ import android.graphics.Point; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.browser.SessionStore; -import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.ui.views.settings.RadioGroupSetting; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; @@ -34,7 +33,7 @@ private void initialize(Context aContext) { mDelegate.showView(new LanguageOptionsView(getContext(), mWidgetManager)); }); header.setHelpClickListener(view -> { - SessionStore.get().loadUri(getResources().getString(R.string.sumo_language_voice_url)); + SessionStore.get().getActiveStore().loadUri(getResources().getString(R.string.sumo_language_voice_url)); mDelegate.exitWholeSettings(); }); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/utils/LocaleUtils.java b/app/src/common/shared/org/mozilla/vrbrowser/utils/LocaleUtils.java index 0371c1e10..83c120f73 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/LocaleUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/LocaleUtils.java @@ -9,8 +9,8 @@ import androidx.annotation.NonNull; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.browser.SessionStore; import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.ui.adapters.Language; import java.util.ArrayList; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/utils/ViewUtils.java b/app/src/common/shared/org/mozilla/vrbrowser/utils/ViewUtils.java index dd7a0d91b..254d9e33a 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/ViewUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/ViewUtils.java @@ -8,6 +8,7 @@ import android.text.style.ClickableSpan; import android.text.style.URLSpan; import android.view.View; +import android.view.ViewGroup; import android.view.ViewParent; import android.widget.TextView; @@ -88,4 +89,14 @@ public static Spanned getSpannedText(String text) { } } + public static boolean isChildrenOf(@NonNull View parent, @NonNull View view) { + if (parent == null || view == null) + return false; + + if (!(parent instanceof ViewGroup)) + return false; + + return parent.findViewById(view.getId()) != null; + } + } diff --git a/app/src/main/cpp/BrowserWorld.cpp b/app/src/main/cpp/BrowserWorld.cpp index e555b90cf..ed3a89a43 100644 --- a/app/src/main/cpp/BrowserWorld.cpp +++ b/app/src/main/cpp/BrowserWorld.cpp @@ -367,7 +367,12 @@ BrowserWorld::State::UpdateControllers(bool& aRelayoutWidgets) { float hitDistance = farClip; vrb::Vector hitPoint; vrb::Vector hitNormal; + for (const WidgetPtr& widget: widgets) { + if (resizingWidget && resizingWidget->IsResizingActive() && resizingWidget != widget) { + // Don't interact with other widgets when resizing gesture is active. + continue; + } vrb::Vector result; vrb::Vector normal; float distance = 0.0f; @@ -923,11 +928,11 @@ BrowserWorld::RemoveWidget(int32_t aHandle) { } void -BrowserWorld::StartWidgetResize(int32_t aHandle) { +BrowserWorld::StartWidgetResize(int32_t aHandle, const vrb::Vector& aMaxSize) { ASSERT_ON_RENDER_THREAD(); WidgetPtr widget = m.GetWidget(aHandle); if (widget) { - widget->StartResize(); + widget->StartResize(aMaxSize); } } @@ -1401,14 +1406,20 @@ JNI_METHOD(void, updateWidgetNative) } } +JNI_METHOD(void, updateVisibleWidgetsNative) +(JNIEnv* aEnv, jobject) { + crow::BrowserWorld::Instance().UpdateVisibleWidgets(); +} + + JNI_METHOD(void, removeWidgetNative) (JNIEnv*, jobject, jint aHandle) { crow::BrowserWorld::Instance().RemoveWidget(aHandle); } JNI_METHOD(void, startWidgetResizeNative) -(JNIEnv*, jobject, jint aHandle) { - crow::BrowserWorld::Instance().StartWidgetResize(aHandle); +(JNIEnv*, jobject, jint aHandle, jfloat aMaxWidth, jfloat aMaxHeight) { + crow::BrowserWorld::Instance().StartWidgetResize(aHandle, vrb::Vector(aMaxWidth, aMaxHeight, 0.0f)); } JNI_METHOD(void, finishWidgetResizeNative) diff --git a/app/src/main/cpp/BrowserWorld.h b/app/src/main/cpp/BrowserWorld.h index 7cfebf3e1..1bda598b0 100644 --- a/app/src/main/cpp/BrowserWorld.h +++ b/app/src/main/cpp/BrowserWorld.h @@ -46,7 +46,7 @@ class BrowserWorld { void AddWidget(int32_t aHandle, const WidgetPlacementPtr& placement); void UpdateWidget(int32_t aHandle, const WidgetPlacementPtr& aPlacement); void RemoveWidget(int32_t aHandle); - void StartWidgetResize(int32_t aHandle); + void StartWidgetResize(int32_t aHandle, const vrb::Vector& aMaxSize); void FinishWidgetResize(int32_t aHandle); void StartWidgetMove(int32_t aHandle, const int32_t aMoveBehavour); void FinishWidgetMove(); diff --git a/app/src/main/cpp/Cylinder.cpp b/app/src/main/cpp/Cylinder.cpp index b570626e7..2a7b81077 100644 --- a/app/src/main/cpp/Cylinder.cpp +++ b/app/src/main/cpp/Cylinder.cpp @@ -23,6 +23,11 @@ namespace crow { +// Ratio between world size and cylinder surface size. +// It should match the values defined in WindowWidget. +// 800px is the default window size for a 4m world size. +float Cylinder::kWorldDensityRatio = 800.0f / 4.0f; + struct Cylinder::State { vrb::CreationContextWeak context; VRLayerCylinderPtr layer; @@ -508,6 +513,49 @@ float Cylinder::DistanceToBackPlane(const vrb::Vector &aStartPoint, const vrb::V return result; } +// Returns the circle angle of a local point in a cylinder +float +Cylinder::GetCylinderAngle(const vrb::Vector& aLocalPoint) const { + return atan2f(-aLocalPoint.z(), aLocalPoint.x()); +} + +vrb::Vector +Cylinder::ProjectPointToQuad(const vrb::Vector& aWorldPoint, const float aAnchorX, const float aCylinderDensity, const vrb::Vector& aMin, const vrb::Vector& aMax) const { + // For cylinders we want to map the position in the cylinder to the position it would have on a quad. + // This way we can reuse the same resize logic between quads and cylinders. + // First Convert to world point to local point in the cylinder. + vrb::Matrix modelView = GetTransformNode()->GetWorldTransform().AfineInverse(); + vrb::Vector localPoint = modelView.MultiplyPosition(aWorldPoint); + const float pointAngle = GetCylinderAngle(localPoint); + + // Ratio used to convert arc length to quad width. + const float thetaRatio = aCylinderDensity * 0.5f / ((float) M_PI * kWorldDensityRatio); + + float x; + + // Handle different anchor points. + if (aAnchorX == 1.0f) { + // Difference between pointer angle and the right anchor point. + const float initialTheta = (aMax.x() - aMin.x()) / thetaRatio; + const float initialAngle = (float)M_PI * 0.5f - initialTheta * 0.5f; + const float arc = fabsf(pointAngle - initialAngle); + x = aMax.x() - arc * thetaRatio; + } else if (aAnchorX == 0.0f) { + // Difference between pointer angle and the left anchor point. + const float initialTheta = (aMax.x() - aMin.x()) / thetaRatio; + const float initialAngle = (float)M_PI * 0.5f + initialTheta * 0.5f; + const float arc = fabsf(initialAngle - pointAngle); + x = aMin.x() + arc * thetaRatio; + } else { // Anchor 0.5f + // The center of the cylinder is 90ยบ. + x = ((float) M_PI * 0.5f - pointAngle) * thetaRatio; + } + + // The mapped position on a quad. + const float y = (aMax.y() - aMin.y()) * localPoint.y() * 0.5f; + return vrb::Vector(x, y, 0.0f); +} + Cylinder::Cylinder(State& aState, vrb::CreationContextPtr& aContext) : m(aState) { m.context = aContext; } diff --git a/app/src/main/cpp/Cylinder.h b/app/src/main/cpp/Cylinder.h index bedbd87cc..d378a4108 100644 --- a/app/src/main/cpp/Cylinder.h +++ b/app/src/main/cpp/Cylinder.h @@ -29,6 +29,7 @@ class Cylinder { static CylinderPtr Create(vrb::CreationContextPtr aContext, const float aRadius, const float aHeight, const VRLayerCylinderPtr& aLayer = nullptr); static CylinderPtr Create(vrb::CreationContextPtr aContext, const float aRadius, const float aHeight, const vrb::Color& aSolidColor, const float kBorder, const vrb::Color& aBorderColor); static CylinderPtr Create(vrb::CreationContextPtr aContext, const VRLayerCylinderPtr& aLayer = nullptr); + static float kWorldDensityRatio; void GetTextureSize(int32_t& aWidth, int32_t& aHeight) const; void SetTextureSize(int32_t aWidth, int32_t aHeight); void SetTexture(const vrb::TexturePtr& aTexture, int32_t aWidth, int32_t aHeight); @@ -49,6 +50,8 @@ class Cylinder { void ConvertToQuadCoordinates(const vrb::Vector& point, float& aX, float& aY, bool aClamp) const; void ConvertFromQuadCoordinates(const float aX, const float aY, vrb::Vector& aWorldPoint, vrb::Vector& aNormal); float DistanceToBackPlane(const vrb::Vector& aStartPoint, const vrb::Vector& aDirection) const; + float GetCylinderAngle(const vrb::Vector& aLocalPoint) const; + vrb::Vector ProjectPointToQuad(const vrb::Vector& aWorldPoint, const float aAnchorX, const float aDensity, const vrb::Vector& aMin, const vrb::Vector& aMax) const; protected: struct State; Cylinder(State& aState, vrb::CreationContextPtr& aContext); diff --git a/app/src/main/cpp/Widget.cpp b/app/src/main/cpp/Widget.cpp index 5ca64176f..1f537c8bb 100644 --- a/app/src/main/cpp/Widget.cpp +++ b/app/src/main/cpp/Widget.cpp @@ -37,6 +37,7 @@ struct Widget::State { float cylinderDensity; vrb::TogglePtr root; vrb::TransformPtr transform; + vrb::TransformPtr transformContainer; // Used for cylinder rotations vrb::TextureSurfacePtr surface; WidgetPlacementPtr placement; WidgetResizerPtr resizer; @@ -66,8 +67,10 @@ struct Widget::State { vrb::CreationContextPtr create = render->GetRenderThreadCreationContext(); transform = vrb::Transform::Create(create); + transformContainer = vrb::Transform::Create(create); root = vrb::Toggle::Create(create); - root->AddNode(transform); + root->AddNode(transformContainer); + transformContainer->AddNode(transform); if (quad) { transform->AddNode(quad->GetRoot()); } else { @@ -126,34 +129,37 @@ struct Widget::State { void UpdateCylinderMatrix() { float w = WorldWidth(); float h = WorldHeight(); - int32_t textureWidth, textureHeight; - cylinder->GetTextureSize(textureWidth, textureHeight); const float radius = cylinder->GetCylinderRadius(); - const float surfaceWidth = (float)textureWidth / (placement->density * placement->textureScale); - const float surfaceHeight = (float)textureHeight / (placement->density * placement->textureScale); + // Compute the arc length and height of the curved surface. + const float surfaceWidth = w * Cylinder::kWorldDensityRatio; + const float surfaceHeight = h * Cylinder::kWorldDensityRatio; // Cylinder density measures the pixels for a 360 cylinder // Oculus recommends 4680px density, which is 13 pixels per degree. const float theta = (float)M_PI * surfaceWidth / (cylinderDensity * 0.5f); cylinder->SetCylinderTheta(theta); + // Compute the height scale of the cylinder such that the world width and height of a 1px element remain square. const float heightScale = surfaceHeight * (float)M_PI / cylinderDensity; + // Scale the cylinder so that widget height matches cylinder height. const float scale = h / (cylinder->GetCylinderHeight() * heightScale); vrb::Matrix scaleMatrix = vrb::Matrix::Identity(); scaleMatrix.ScaleInPlace(vrb::Vector(radius * scale, radius * scale * heightScale, radius * scale)); + // Translate the z of the cylinder to make the back of the curved surface the z position anchor point. vrb::Matrix translation = vrb::Matrix::Translation(vrb::Vector(0.0f, 0.0f, radius * scale)); - - const float x = transform->GetWorldTransform().GetTranslation().x(); - if (x != 0.0f) { + cylinder->SetTransform(translation.PostMultiply(scaleMatrix)); + const float x = transform->GetTransform().GetTranslation().x(); + if (x != 0.0f && placement->cylinderMapRadius > 0) { // Automatically adjust correct yaw angle & position for the cylinders not centered on the X axis const float r = radius * scale; const float perimeter = 2.0f * r * (float)M_PI; - const float angle = 0.5f * (float)M_PI - x / perimeter * 2.0f * (float)M_PI; - vrb::Matrix rotation = vrb::Matrix::Rotation(vrb::Vector(-cosf(angle), 0.0f, sinf(angle))); - vrb::Matrix transform = translation.PostMultiply(scaleMatrix).PostMultiply(rotation); - transform.PreMultiplyInPlace(vrb::Matrix::Translation(vrb::Vector(-x, 0.0f, 0.0f))); - cylinder->SetTransform(transform); + float angle = 0.5f * (float)M_PI - x / perimeter * 2.0f * (float)M_PI; + float delta = placement->cylinderMapRadius - radius * scale; + vrb::Matrix transform = vrb::Matrix::Rotation(vrb::Vector(-cosf(angle), 0.0f, sinf(angle))); + transform.PreMultiplyInPlace(vrb::Matrix::Translation(vrb::Vector(0.0f, 0.0f, -delta))); + transform.PostMultiplyInPlace(vrb::Matrix::Translation(vrb::Vector(-x, 0.0f, delta))); + transformContainer->SetTransform(transform); } else { - cylinder->SetTransform(translation.PostMultiply(scaleMatrix)); + transformContainer->SetTransform(vrb::Matrix::Identity()); } } @@ -266,7 +272,7 @@ bool Widget::TestControllerIntersection(const vrb::Vector& aStartPoint, const vrb::Vector& aDirection, vrb::Vector& aResult, vrb::Vector& aNormal, const bool aClamp, bool& aIsInWidget, float& aDistance) const { aDistance = -1.0f; - if (!m.root->IsEnabled(*m.transform)) { + if (!m.root->IsEnabled(*m.transformContainer)) { return false; } @@ -353,6 +359,7 @@ Widget::SetQuad(const QuadPtr& aQuad) const { m.quad = aQuad; m.transform->AddNode(aQuad->GetRoot()); + m.transformContainer->SetTransform(vrb::Matrix::Identity()); m.RemoveResizer(); m.UpdateSurface(textureWidth, textureHeight); @@ -404,7 +411,7 @@ Widget::SetPlacement(const WidgetPlacementPtr& aPlacement) { } void -Widget::StartResize() { +Widget::StartResize(const vrb::Vector& aMaxSize) { vrb::Vector worldMin, worldMax; GetWidgetMinAndMax(worldMin, worldMax); if (m.resizer) { @@ -418,6 +425,7 @@ Widget::StartResize() { m.resizer = WidgetResizer::Create(create, this); m.transform->InsertNode(m.resizer->GetRoot(), 0); } + m.resizer->SetMaxSize(aMaxSize); m.resizing = true; m.resizer->ToggleVisible(true); if (m.quad) { @@ -444,13 +452,18 @@ Widget::IsResizing() const { return m.resizing; } +bool +Widget::IsResizingActive() const { + return m.resizing && m.resizer->IsActive(); +} + void Widget::HandleResize(const vrb::Vector& aPoint, bool aPressed, bool& aResized, bool &aResizeEnded) { m.resizer->HandleResizeGestures(aPoint, aPressed, aResized, aResizeEnded); if (aResized || aResizeEnded) { - m.min = m.resizer->GetCurrentMin(); - m.max = m.resizer->GetCurrentMax(); + m.min = m.resizer->GetResizeMin(); + m.max = m.resizer->GetResizeMax(); if (m.quad) { m.quad->SetWorldSize(m.min, m.max); } else if (m.cylinder) { @@ -474,6 +487,11 @@ Widget::SetCylinderDensity(const float aDensity) { } } +float +Widget::GetCylinderDensity() const { + return m.cylinderDensity; +} + Widget::Widget(State& aState, vrb::RenderContextPtr& aContext) : m(aState) { m.context = aContext; } diff --git a/app/src/main/cpp/Widget.h b/app/src/main/cpp/Widget.h index af4d8b16f..d1f612e3f 100644 --- a/app/src/main/cpp/Widget.h +++ b/app/src/main/cpp/Widget.h @@ -66,12 +66,14 @@ class Widget { vrb::TransformPtr GetTransformNode() const; const WidgetPlacementPtr& GetPlacement() const; void SetPlacement(const WidgetPlacementPtr& aPlacement); - void StartResize(); + void StartResize(const vrb::Vector& aMaxSize); void FinishResize(); bool IsResizing() const; + bool IsResizingActive() const; void HandleResize(const vrb::Vector& aPoint, bool aPressed, bool& aResized, bool &aResizeEnded); void HoverExitResize(); void SetCylinderDensity(const float aDensity); + float GetCylinderDensity() const; protected: struct State; Widget(State& aState, vrb::RenderContextPtr& aContext); diff --git a/app/src/main/cpp/WidgetMover.cpp b/app/src/main/cpp/WidgetMover.cpp index d83c14ad9..cb24e7a6d 100644 --- a/app/src/main/cpp/WidgetMover.cpp +++ b/app/src/main/cpp/WidgetMover.cpp @@ -7,8 +7,10 @@ #include "Widget.h" #include "WidgetPlacement.h" #include "VRBrowser.h" +#include "Cylinder.h" #include "vrb/ConcreteClass.h" #include "vrb/Matrix.h" +#include "vrb/Transform.h" namespace crow { @@ -37,18 +39,29 @@ struct WidgetMover::State { , endRotation(0) {} + vrb::Vector ProjectPoint(const vrb::Vector& aWorldPoint) const { + if (widget->GetCylinder()) { + vrb::Vector min, max; + widget->GetWidgetMinAndMax(min, max); + vrb::Vector point = widget->GetCylinder()->ProjectPointToQuad(aWorldPoint, 0.5f, widget->GetCylinderDensity(), min, max); + return widget->GetTransformNode()->GetWorldTransform().MultiplyPosition(point); + } else { + return aWorldPoint; + } + } WidgetPlacementPtr& HandleKeyboardMove(const vrb::Vector& aDelta) { float x = initialPlacement->translation.x() * WidgetPlacement::kWorldDPIRatio; float y = initialPlacement->translation.y() * WidgetPlacement::kWorldDPIRatio; + const float windowZ = -4.2f; // Must match window_world_z in dimen.xml const float maxX = 4.0f; // Relative to 0.5f anchor point. const float minX = -maxX; - const float maxY = 2.0f; // Relative to 0.0f anchor point. - const float minY = -1.1f; // Relative to 0.0f anchor point. + const float maxY = 1.8f; // Relative to 0.0f anchor point. + const float minY = -1.3f; // Relative to 0.0f anchor point. const float maxAngle = -35.0f * (float)M_PI / 180.0f; const float angleStartY = 0.8f; - const float minZ = -2.5f; - const float maxZ = -3.2f; + const float minZ = -2.5f - windowZ; + const float maxZ = -3.2f - windowZ; const float thresholdZ = 1.45f; x += aDelta.x(); y += aDelta.y(); @@ -108,8 +121,14 @@ WidgetMover::HandleMove(const vrb::Vector& aStart, const vrb::Vector& aDirection if (hitDistance < 0) { return nullptr; }; + hitPoint = m.ProjectPoint(hitPoint); - const vrb::Vector delta = hitPoint - m.initialPoint; + vrb::Vector delta = hitPoint - m.initialPoint; + delta.y() = hitPoint.y() - m.initialPoint.y(); + delta.x() = vrb::Vector(hitPoint.x() - m.initialPoint.x(), 0.0f, hitPoint.z() - m.initialPoint.z()).Magnitude(); + if (hitPoint.x() < m.initialPoint.x()) { + delta.x() *= -1.0f; + } if (m.moveBehaviour == WidgetMoveBehaviour::KEYBOARD) { return m.HandleKeyboardMove(delta); @@ -131,7 +150,7 @@ WidgetMover::StartMoving(const WidgetPtr& aWidget, const int32_t aMoveBehaviour, m.widget = aWidget; m.attachedController = aControllerIndex; m.initialTransform = aWidget->GetTransform(); - m.initialPoint = aHitPoint; + m.initialPoint = m.ProjectPoint(aHitPoint); m.anchorPoint = aAnchorPoint; m.initialPlacement = aWidget->GetPlacement(); m.movePlacement = WidgetPlacement::Create(*m.initialPlacement); diff --git a/app/src/main/cpp/WidgetPlacement.cpp b/app/src/main/cpp/WidgetPlacement.cpp index efbe6f3bc..ea071b864 100644 --- a/app/src/main/cpp/WidgetPlacement.cpp +++ b/app/src/main/cpp/WidgetPlacement.cpp @@ -55,8 +55,9 @@ WidgetPlacement::FromJava(JNIEnv* aEnv, jobject& aObject) { GET_BOOLEAN_FIELD(showPointer); GET_BOOLEAN_FIELD(firstDraw); GET_BOOLEAN_FIELD(layer); - GET_BOOLEAN_FIELD(cylinder); GET_FLOAT_FIELD(textureScale, "textureScale"); + GET_BOOLEAN_FIELD(cylinder); + GET_FLOAT_FIELD(cylinderMapRadius, "cylinderMapRadius"); return result; } diff --git a/app/src/main/cpp/WidgetPlacement.h b/app/src/main/cpp/WidgetPlacement.h index b1ea287f1..b68e9c0bf 100644 --- a/app/src/main/cpp/WidgetPlacement.h +++ b/app/src/main/cpp/WidgetPlacement.h @@ -31,8 +31,9 @@ struct WidgetPlacement { bool showPointer; bool firstDraw; bool layer; - bool cylinder; float textureScale; + bool cylinder; + float cylinderMapRadius; int32_t GetTextureWidth() const; int32_t GetTextureHeight() const; diff --git a/app/src/main/cpp/WidgetResizer.cpp b/app/src/main/cpp/WidgetResizer.cpp index f7f9863a3..07d0153b3 100644 --- a/app/src/main/cpp/WidgetResizer.cpp +++ b/app/src/main/cpp/WidgetResizer.cpp @@ -4,6 +4,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "WidgetResizer.h" +#include "WidgetPlacement.h" #include "Widget.h" #include "Cylinder.h" #include "Quad.h" @@ -52,8 +53,8 @@ static const float kBarSize = 0.04f; static const float kBorder = kBarSize * 0.15f; #endif static const float kHandleRadius = 0.08f; -static const vrb::Vector kMinResize(1.5f, 1.5f, 0.0f); -static const vrb::Vector kMaxResize(8.0f, 4.5f, 0.0f); +static const vrb::Vector kDefaultMinResize(1.5f, 1.5f, 0.0f); +static const vrb::Vector kDefaultMaxResize(8.0f, 4.5f, 0.0f); static vrb::Color kDefaultColor(0x2BD5D5FF); static vrb::Color kHoverColor(0xf7ce4dff); static vrb::Color kActiveColor(0xf7ce4dff); @@ -92,12 +93,11 @@ struct ResizeBar { if (aMode == ResizeBar::Mode::Cylinder) { result->cylinder = Cylinder::Create(aContext, 1.0f, kBarSize, vrb::Color(1.0f, 1.0f, 1.0f, 1.0f), kBorder, vrb::Color(1.0f, 1.0f, 1.0f, 0.0f)); result->cylinder->SetLightsEnabled(false); - if (aScale.x() == 1.0f) { - // Fix sticking out border at the bottom of the resize bar (No handles to hide it...) - vrb::TextureGLPtr defaultTexture = aContext->GetDefaultTexture(); - result->cylinder->SetTexture(defaultTexture, defaultTexture->GetWidth(), defaultTexture->GetHeight()); - result->cylinder->GetRenderState()->SetCustomFragmentShader(sCylinderFragmentShader); - } + // Fix sticking out borders + // Sometimes there is no handle to hide it (e.e. bottom bar and anchor points != 0.5f) + vrb::TextureGLPtr defaultTexture = aContext->GetDefaultTexture(); + result->cylinder->SetTexture(defaultTexture, defaultTexture->GetWidth(), defaultTexture->GetHeight()); + result->cylinder->GetRenderState()->SetCustomFragmentShader(sCylinderFragmentShader); result->transform->AddNode(result->cylinder->GetRoot()); } else { result->geometry = CreateGeometry(aContext, -max, max, aBorder); @@ -237,6 +237,8 @@ struct ResizeHandle { result->geometry = ResizeHandle::CreateGeometry(aContext); result->transform = vrb::Transform::Create(aContext); result->transform->AddNode(result->geometry); + result->root = vrb::Toggle::Create(aContext); + result->root->AddNode(result->transform); result->resizeState = ResizeState ::Default; UpdateResizeMaterial(result->geometry->GetRenderState(), result->resizeState); return result; @@ -336,13 +338,22 @@ struct ResizeHandle { return geometry; } + void SetVisible(const bool aVisible) { + if (visible != aVisible) { + root->ToggleAll(aVisible); + visible = aVisible; + } + } + vrb::Vector center; ResizeMode resizeMode; std::vector attachedBars; vrb::GeometryPtr geometry; + vrb::TogglePtr root; vrb::TransformPtr transform; ResizeState resizeState; float touchRatio; + bool visible = true; }; struct WidgetResizer::State { @@ -355,6 +366,7 @@ struct WidgetResizer::State { vrb::Vector currentMin; vrb::Vector currentMax; vrb::Vector pointerOffset; + vrb::Vector maxSize; bool resizing; vrb::TogglePtr root; std::vector resizeHandles; @@ -376,6 +388,7 @@ struct WidgetResizer::State { root = vrb::Toggle::Create(create); currentMin = min; currentMax = max; + maxSize = kDefaultMaxResize; vrb::Vector horizontalSize(0.0f, 0.5f, 0.0f); vrb::Vector verticalSize(0.5f, 0.0f, 0.0f); @@ -425,7 +438,7 @@ struct WidgetResizer::State { ResizeHandlePtr result = ResizeHandle::Create(create, aCenter, aResizeMode, aBars); result->touchRatio = aTouchRatio; resizeHandles.push_back(result); - root->InsertNode(result->transform, 0); + root->InsertNode(result->root, 0); return result; } @@ -438,20 +451,11 @@ struct WidgetResizer::State { } vrb::Vector ProjectPoint(const vrb::Vector& aWorldPoint) const { - vrb::Matrix modelView = widget->GetTransformNode()->GetWorldTransform().AfineInverse(); if (widget->GetCylinder()) { - // Map the position in the cylinder to the position it would have on a quad - const float radius = widget->GetCylinder()->GetTransformNode()->GetTransform().GetScale().x(); - float cosAngle = fminf(1.0f, fabsf(aWorldPoint.x()) / radius); - const float sign = aWorldPoint.x() > 0 ? 1.0f : -1.0f; - const float angle = acosf(cosAngle); - const float surfaceArc = (float)M_PI - angle * 2.0f; - const float projectedWidth = WorldWidth() * surfaceArc / widget->GetCylinder()->GetCylinderTheta(); - vrb::Vector point(projectedWidth * 0.5f * sign, aWorldPoint.y(), aWorldPoint.z()); - vrb::Vector result = modelView.MultiplyPosition(point); - result.z() = 0.0f; - return result; + return widget->GetCylinder()->ProjectPointToQuad(aWorldPoint, GetAnchorX(), widget->GetCylinderDensity(), min, max); } else { + // For quads just convert to world point to local point. + vrb::Matrix modelView = widget->GetTransformNode()->GetWorldTransform().AfineInverse(); return modelView.MultiplyPosition(aWorldPoint); } } @@ -484,10 +488,21 @@ struct WidgetResizer::State { widget->GetCylinder()->GetTextureSize(textureWidth, textureHeight); vrb::Matrix modelView = widget->GetTransformNode()->GetWorldTransform().AfineInverse(); + // Delta for x anchor point != 0.5f. + float centerX = 0.0f; + const float anchorX = GetAnchorX(); + if (anchorX == 1.0f) { + centerX = min.x() + width * 0.5f; + } else if (anchorX == 0.0f) { + centerX = max.x() - width * 0.5f; + } + const float perimeter = 2.0f * radius * (float)M_PI; + float angleDelta = centerX / perimeter * 2.0f * (float)M_PI; + for (ResizeBarPtr& bar: resizeBars) { float targetWidth = bar->scale.x() > 0.0f ? (bar->scale.x() * fabsf(width)) + kBarSize : kBarSize; float targetHeight = bar->scale.y() > 0.0f ? (bar->scale.y() * fabs(height)) + kBarSize : kBarSize; - const float pointerAngle = (float)M_PI * 0.5f + theta * 0.5f - theta * bar->center.x(); + float pointerAngle = (float)M_PI * 0.5f + theta * 0.5f - theta * bar->center.x() + angleDelta; vrb::Matrix rotation = vrb::Matrix::Rotation(vrb::Vector(-cosf(pointerAngle), 0.0f, sinf(pointerAngle))); if (bar->cylinder) { bar->cylinder->SetCylinderTheta(theta * bar->scale.x()); @@ -506,7 +521,7 @@ struct WidgetResizer::State { } for (ResizeHandlePtr& handle: resizeHandles) { - const float pointerAngle = (float)M_PI * 0.5f + theta * 0.5f - theta * handle->center.x(); + const float pointerAngle = (float)M_PI * 0.5f + theta * 0.5f - theta * handle->center.x() + angleDelta; vrb::Matrix translation = vrb::Matrix::Position(vrb::Vector(radius * cosf(pointerAngle), min.y() + height * handle->center.y(), radius - radius * sinf(pointerAngle))); @@ -515,7 +530,24 @@ struct WidgetResizer::State { } } + void UpdateVisibleHandles() { + float anchorX = GetAnchorX(); + + for (ResizeHandlePtr & handle: resizeHandles) { + handle->SetVisible(handle->center.x() == 0.5f || (handle->center.x() != anchorX)); + } + } + + float GetAnchorX() const { + if (widget && widget->GetPlacement()) { + return widget->GetPlacement()->anchor.x(); + } + + return 0.5f; + } + void Layout() { + UpdateVisibleHandles(); if (widget->GetCylinder()) { LayoutCylinder(); } else { @@ -525,6 +557,9 @@ struct WidgetResizer::State { ResizeHandlePtr GetIntersectingHandler(const vrb::Vector& point) { for (const ResizeHandlePtr& handle: resizeHandles) { + if (!handle->visible) { + continue; + } vrb::Vector worldCenter(min.x() + WorldWidth() * handle->center.x(), min.y() + WorldHeight() * handle->center.y(), 0.0f); float distance = (point - worldCenter).Magnitude(); if (distance < kHandleRadius * handle->touchRatio) { @@ -545,6 +580,11 @@ struct WidgetResizer::State { float originalAspect = originalWidth / originalHeight; float width = fabsf(point.x()) * 2.0f; + if (widget->GetPlacement()->anchor.x() == 1.0f) { + width = fabsf(max.x() - point.x()); + } else if (widget->GetPlacement()->anchor.x() == 0.0f) { + width = fabsf(point.x() - min.x()); + } float height = fabsf(point.y() - min.y()); // Calculate resize based on resize mode @@ -560,8 +600,8 @@ struct WidgetResizer::State { } // Clamp to max and min resize sizes - width = fmaxf(fminf(width, kMaxResize.x()), kMinResize.x()); - height = fmaxf(fminf(height, kMaxResize.y()), kMinResize.y()); + width = fmaxf(fminf(width, maxSize.x()), kDefaultMinResize.x()); + height = fmaxf(fminf(height, maxSize.y()), kDefaultMinResize.y()); if (keepAspect) { height = width / originalAspect; } @@ -570,7 +610,7 @@ struct WidgetResizer::State { currentMax = vrb::Vector(width * 0.5f, height * 0.5f, 0.0f); // Reset world min and max points with the new resize values - if (!widget->GetCylinder() || keepAspect) { + if (!widget->GetCylinder()) { min = currentMin; max = currentMax; } @@ -603,6 +643,11 @@ WidgetResizer::SetSize(const vrb::Vector& aMin, const vrb::Vector& aMax) { m.Layout(); } +void +WidgetResizer::SetMaxSize(const vrb::Vector& aMaxSize) { + m.maxSize = aMaxSize; +} + void WidgetResizer::ToggleVisible(bool aVisible) { m.root->ToggleAll(aVisible); @@ -614,9 +659,6 @@ WidgetResizer::TestIntersection(const vrb::Vector& aWorldPoint) const { return true; } const vrb::Vector point = m.ProjectPoint(aWorldPoint); - if (m.widget->GetCylinder()) { - //point = - } vrb::Vector extraMin = vrb::Vector(m.min.x() - kBarSize * 0.5f, m.min.y() - kBarSize * 0.5f, 0.0f); vrb::Vector extraMax = vrb::Vector(m.max.x() + kBarSize * 0.5f, m.max.y() + kBarSize * 0.5f, 0.0f); @@ -685,15 +727,20 @@ WidgetResizer::HoverExitResize() { } const vrb::Vector& -WidgetResizer::GetCurrentMin() const { +WidgetResizer::GetResizeMin() const { return m.min; } const vrb::Vector& -WidgetResizer::GetCurrentMax() const { +WidgetResizer::GetResizeMax() const { return m.max; } +bool +WidgetResizer::IsActive() const { + return m.activeHandle && m.activeHandle->resizeState == ResizeState::Active; +} + WidgetResizer::WidgetResizer(State& aState, vrb::CreationContextPtr& aContext) : m(aState) { m.context = aContext; diff --git a/app/src/main/cpp/WidgetResizer.h b/app/src/main/cpp/WidgetResizer.h index 8b91df149..76b42eab1 100644 --- a/app/src/main/cpp/WidgetResizer.h +++ b/app/src/main/cpp/WidgetResizer.h @@ -25,12 +25,14 @@ class WidgetResizer { static WidgetResizerPtr Create(vrb::CreationContextPtr& aContext, Widget * aWidget); vrb::NodePtr GetRoot() const; void SetSize(const vrb::Vector& aMin, const vrb::Vector& aMax); + void SetMaxSize(const vrb::Vector& aMaxSize); void ToggleVisible(bool aVisible); bool TestIntersection(const vrb::Vector& point) const; void HandleResizeGestures(const vrb::Vector& aPoint, bool aPressed, bool& aResized, bool &aResizeEnded); void HoverExitResize(); - const vrb::Vector& GetCurrentMin() const; - const vrb::Vector& GetCurrentMax() const; + const vrb::Vector& GetResizeMin() const; + const vrb::Vector& GetResizeMax() const; + bool IsActive() const; protected: struct State; WidgetResizer(State& aState, vrb::CreationContextPtr& aContext); diff --git a/app/src/main/res/color/context_menu_icon_color.xml b/app/src/main/res/color/context_menu_icon_color.xml new file mode 100644 index 000000000..c944e626d --- /dev/null +++ b/app/src/main/res/color/context_menu_icon_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/context_menu_text_color.xml b/app/src/main/res/color/context_menu_text_color.xml new file mode 100644 index 000000000..c944e626d --- /dev/null +++ b/app/src/main/res/color/context_menu_text_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/context_menu_background.xml b/app/src/main/res/drawable/context_menu_background.xml new file mode 100644 index 000000000..984f1f525 --- /dev/null +++ b/app/src/main/res/drawable/context_menu_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/context_menu_item_background_color.xml b/app/src/main/res/drawable/context_menu_item_background_color.xml new file mode 100644 index 000000000..c0295c495 --- /dev/null +++ b/app/src/main/res/drawable/context_menu_item_background_color.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fullscreen_button_first.xml b/app/src/main/res/drawable/fullscreen_button_first.xml index a3aed5ebc..901222966 100644 --- a/app/src/main/res/drawable/fullscreen_button_first.xml +++ b/app/src/main/res/drawable/fullscreen_button_first.xml @@ -1,5 +1,12 @@ + + + + + + + diff --git a/app/src/main/res/drawable/fullscreen_button_last.xml b/app/src/main/res/drawable/fullscreen_button_last.xml index 7bd2a9114..82d2190b1 100644 --- a/app/src/main/res/drawable/fullscreen_button_last.xml +++ b/app/src/main/res/drawable/fullscreen_button_last.xml @@ -1,5 +1,12 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_context_menu_new_window.xml b/app/src/main/res/drawable/ic_context_menu_new_window.xml new file mode 100644 index 000000000..f9d08e062 --- /dev/null +++ b/app/src/main/res/drawable/ic_context_menu_new_window.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_icon_private_browsing.xml b/app/src/main/res/drawable/ic_icon_private_browsing.xml deleted file mode 100644 index b03db0293..000000000 --- a/app/src/main/res/drawable/ic_icon_private_browsing.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_icon_private_browsing_on.xml b/app/src/main/res/drawable/ic_icon_private_browsing_on.xml deleted file mode 100644 index bae8327c5..000000000 --- a/app/src/main/res/drawable/ic_icon_private_browsing_on.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_icon_settings.xml b/app/src/main/res/drawable/ic_icon_settings.xml deleted file mode 100644 index 86d4502cb..000000000 --- a/app/src/main/res/drawable/ic_icon_settings.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_icon_tray_bookmarks_v2.xml b/app/src/main/res/drawable/ic_icon_tray_bookmarks_v2.xml new file mode 100644 index 000000000..7f272d0f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_tray_bookmarks_v2.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_icon_tray_help_v2.xml b/app/src/main/res/drawable/ic_icon_tray_help_v2.xml index 527f5a2b9..386d16f98 100644 --- a/app/src/main/res/drawable/ic_icon_tray_help_v2.xml +++ b/app/src/main/res/drawable/ic_icon_tray_help_v2.xml @@ -4,9 +4,9 @@ android:viewportWidth="200" android:viewportHeight="200"> diff --git a/app/src/main/res/drawable/ic_icon_tray_newwindow.xml b/app/src/main/res/drawable/ic_icon_tray_newwindow.xml new file mode 100644 index 000000000..8cef40d33 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_tray_newwindow.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_icon_tray_private_browsing_on_v2.xml b/app/src/main/res/drawable/ic_icon_tray_private_browsing_on_v2.xml new file mode 100644 index 000000000..284be9d2b --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_tray_private_browsing_on_v2.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_icon_tray_private_browsing_v2.xml b/app/src/main/res/drawable/ic_icon_tray_private_browsing_v2.xml new file mode 100644 index 000000000..2eb5d00ca --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_tray_private_browsing_v2.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_icon_tray_settings_v2.xml b/app/src/main/res/drawable/ic_icon_tray_settings_v2.xml new file mode 100644 index 000000000..fd92a83ef --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_tray_settings_v2.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_icon_window_exit.xml b/app/src/main/res/drawable/ic_icon_window_exit.xml new file mode 100644 index 000000000..eef590c04 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_window_exit.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_icon_window_left.xml b/app/src/main/res/drawable/ic_icon_window_left.xml new file mode 100644 index 000000000..b5d21c713 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_window_left.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_icon_window_right.xml b/app/src/main/res/drawable/ic_icon_window_right.xml new file mode 100644 index 000000000..e359b7ce9 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_window_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/context_menu.xml b/app/src/main/res/layout/context_menu.xml new file mode 100644 index 000000000..e305b7a60 --- /dev/null +++ b/app/src/main/res/layout/context_menu.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/context_menu_item.xml b/app/src/main/res/layout/context_menu_item.xml new file mode 100644 index 000000000..9553f716c --- /dev/null +++ b/app/src/main/res/layout/context_menu_item.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/navigation_bar.xml b/app/src/main/res/layout/navigation_bar.xml index 42d149850..c03316e14 100644 --- a/app/src/main/res/layout/navigation_bar.xml +++ b/app/src/main/res/layout/navigation_bar.xml @@ -124,6 +124,13 @@ android:textSize="14sp" android:text="2x" /> + + + + + + + + +