package org.mozilla.vrbrowser.ui.widgets; import android.content.Context; import android.util.Log; import androidx.annotation.IntDef; 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.VRBrowserApplication; import org.mozilla.vrbrowser.browser.Accounts; import org.mozilla.vrbrowser.browser.Media; import org.mozilla.vrbrowser.browser.Services; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.browser.engine.Session; import org.mozilla.vrbrowser.browser.engine.SessionState; import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.widgets.dialogs.PromptDialogWidget; import org.mozilla.vrbrowser.ui.widgets.dialogs.UIDialog; import org.mozilla.vrbrowser.utils.BitmapCache; import org.mozilla.vrbrowser.utils.ConnectivityReceiver; import org.mozilla.vrbrowser.utils.SystemUtils; import org.mozilla.vrbrowser.utils.UrlUtils; 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; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import mozilla.components.concept.sync.AccountObserver; import mozilla.components.concept.sync.AuthType; import mozilla.components.concept.sync.OAuthAccount; import mozilla.components.concept.sync.Profile; import mozilla.components.concept.sync.TabData; import static org.mozilla.vrbrowser.ui.widgets.settings.SettingsView.SettingViewType.FXA; public class Windows implements TrayListener, TopBarWidget.Delegate, TitleBarWidget.Delegate, WindowWidget.WindowListener, TabsWidget.TabDelegate, Services.TabReceivedDelegate { private static final String LOGTAG = SystemUtils.createLogtag(Windows.class); public static final int WHITE = 0xFFFFFFFF; public static final int GRAY = 0x555555FF; @IntDef(value = { OPEN_IN_FOREGROUND, OPEN_IN_BACKGROUND, OPEN_IN_NEW_WINDOW}) public @interface NewTabLocation {} public static final int OPEN_IN_FOREGROUND = 0; public static final int OPEN_IN_BACKGROUND = 1; public static final int OPEN_IN_NEW_WINDOW = 2; private static final String WINDOWS_SAVE_FILENAME = "windows_state.json"; private static final int TAB_ADDED_NOTIFICATION_ID = 0; private static final int TAB_SENT_NOTIFICATION_ID = 1; private static final int BOOKMARK_ADDED_NOTIFICATION_ID = 2; // Restore URLs blocklist private static final List<String> SAVE_BLOCKLIST = Stream.of( "https://accounts.firefox.com/oauth/" ).collect(Collectors.toList()); class WindowState { WindowPlacement placement; int textureWidth; int textureHeight; float worldWidth; int tabIndex = -1; PanelType panelType; public void load(WindowWidget aWindow, WindowsState aState, int aTabIndex) { WidgetPlacement widgetPlacement; if (aWindow.isFullScreen()) { widgetPlacement = aWindow.getBeforeFullscreenPlacement(); placement = aWindow.getWindowPlacementBeforeFullscreen(); } else if (aWindow.isResizing()) { widgetPlacement = aWindow.getBeforeResizePlacement(); placement = aWindow.getWindowPlacement(); } else { widgetPlacement = aWindow.getPlacement(); placement = aWindow.getWindowPlacement(); } textureWidth = widgetPlacement.width; textureHeight = widgetPlacement.height; worldWidth = widgetPlacement.worldWidth; tabIndex = aTabIndex; if (aWindow.isBookmarksVisible()) { panelType = PanelType.BOOKMARKS; } else if (aWindow.isHistoryVisible()) { panelType = PanelType.HISTORY; } else if (aWindow.isDownloadsVisible()) { panelType = PanelType.DOWNLOADS; } else { panelType = PanelType.NONE; } } } class WindowsState { WindowPlacement focusedWindowPlacement = WindowPlacement.FRONT; ArrayList<WindowState> regularWindowsState = new ArrayList<>(); ArrayList<SessionState> tabs = new ArrayList<>(); boolean privateMode = false; } private Context mContext; private WidgetManagerDelegate mWidgetManager; private Delegate mDelegate; private ArrayList<WindowWidget> mRegularWindows; private ArrayList<WindowWidget> mPrivateWindows; private WindowWidget mFocusedWindow; private static int sIndex; private boolean mPrivateMode = false; public static final int MAX_WINDOWS = 3; private WindowWidget mFullscreenWindow; private WindowPlacement mRegularWindowPlacement; private WindowPlacement mPrivateWindowPlacement; private boolean mStoredCurvedMode = false; private boolean mForcedCurvedMode = false; private boolean mIsPaused = false; private TabsWidget mTabsWidget; private Accounts mAccounts; private Services mServices; private PromptDialogWidget mNoInternetDialog; private boolean mCompositorPaused = false; private WindowsState mWindowsState; private boolean mIsRestoreEnabled; private boolean mAfterRestore; private String mAddedTabUri; private @NewTabLocation int mAddedTabLocation = OPEN_IN_FOREGROUND; public enum PanelType { NONE, BOOKMARKS, HISTORY, DOWNLOADS } 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); void onWindowBorderChanged(@NonNull WindowWidget aChangeWindow); void onWindowsMoved(); void onWindowClosed(); void onWindowVideoAvailabilityChanged(@NonNull WindowWidget aWindow); } public Windows(Context aContext) { mContext = aContext; mWidgetManager = (WidgetManagerDelegate) aContext; mRegularWindows = new ArrayList<>(); mPrivateWindows = new ArrayList<>(); mRegularWindowPlacement = WindowPlacement.FRONT; mPrivateWindowPlacement = WindowPlacement.FRONT; mStoredCurvedMode = SettingsStore.getInstance(mContext).getCylinderDensity() > 0.0f; mAccounts = ((VRBrowserApplication)mContext.getApplicationContext()).getAccounts(); mAccounts.addAccountListener(mAccountObserver); mServices = ((VRBrowserApplication)mContext.getApplicationContext()).getServices(); mServices.setTabReceivedDelegate(this); mWidgetManager.addConnectivityListener(mConnectivityDelegate); mIsRestoreEnabled = SettingsStore.getInstance(mContext).isRestoreTabsEnabled(); mWindowsState = restoreState(); restoreWindows(); } public 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.isFullScreen() ? mFocusedWindow.getWindowPlacementBeforeFullscreen() : mFocusedWindow.getWindowPlacement(); ArrayList<Session> sessions = SessionStore.get().getSortedSessions(false); state.tabs = sessions.stream() .map(Session::getSessionState) .filter(sessionState -> SAVE_BLOCKLIST.stream().noneMatch(uri -> sessionState.mUri != null && sessionState.mUri.startsWith(uri) )) .collect(Collectors.toCollection(ArrayList::new)); for (WindowWidget window : mRegularWindows) { if (window.getSession() != null) { WindowState windowState = new WindowState(); windowState.load(window, state, state.tabs.indexOf(window.getSession().getSessionState())); state.regularWindowsState.add(windowState); } } Gson gson = new GsonBuilder().setPrettyPrinting().create(); gson.toJson(state, writer); Log.d(LOGTAG, "Windows state saved"); } catch (IOException e) { Log.e(LOGTAG, "Error saving windows state: " + e.getLocalizedMessage()); file.delete(); } } 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<WindowsState>() {}.getType(); restored = gson.fromJson(reader, type); Log.d(LOGTAG, "Windows state restored"); } catch (Exception e) { Log.w(LOGTAG, "Error restoring windows state: " + e.getLocalizedMessage()); } finally { file.delete(); } return restored; } public void setDelegate(Delegate aDelegate) { mDelegate = aDelegate; } public WindowWidget getFocusedWindow() { if (mFullscreenWindow != null) { return mFullscreenWindow; } return mFocusedWindow; } @Nullable public WindowWidget addWindow() { if (getCurrentWindows().size() >= MAX_WINDOWS) { return null; } if (mFullscreenWindow != null) { mFullscreenWindow.getSession().exitFullScreen(); onFullScreen(mFullscreenWindow, false); } WindowWidget frontWindow = getFrontWindow(); WindowWidget leftWindow = getLeftWindow(); WindowWidget rightWindow = getRightWindow(); WindowWidget newWindow = createWindow(null); 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.FRONT); placeWindow(frontWindow, 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.FRONT); placeWindow(frontWindow, WindowPlacement.LEFT); } updateMaxWindowScales(); mWidgetManager.addWidget(newWindow); focusWindow(newWindow); updateCurvedMode(true); updateViews(); // We are only interested in general windows opened. if (!isInPrivateMode()) { GleanMetricsService.newWindowOpenEvent(); } return newWindow; } private WindowWidget addRestoredWindow(@NonNull WindowState aState, @Nullable Session aSession) { if (getCurrentWindows().size() >= MAX_WINDOWS) { return null; } if (aSession != null) { aSession.setActive(true); } WindowWidget newWindow = createWindow(aSession); newWindow.getPlacement().width = aState.textureWidth; newWindow.getPlacement().height = aState.textureHeight; newWindow.getPlacement().worldWidth = aState.worldWidth; placeWindow(newWindow, aState.placement); if (newWindow.getSession() != null) { if (aState.panelType != null) { switch (aState.panelType) { case BOOKMARKS: newWindow.getSession().loadUri(UrlUtils.ABOUT_BOOKMARKS); break; case HISTORY: newWindow.getSession().loadUri(UrlUtils.ABOUT_HISTORY); break; case DOWNLOADS: newWindow.getSession().loadUri(UrlUtils.ABOUT_DOWNLOADS); break; } } } updateCurvedMode(true); mWidgetManager.addWidget(newWindow); return newWindow; } public void closeWindow(@NonNull WindowWidget aWindow) { WindowWidget frontWindow = getFrontWindow(); WindowWidget leftWindow = getLeftWindow(); WindowWidget rightWindow = getRightWindow(); aWindow.hidePanel(PanelType.BOOKMARKS); aWindow.hidePanel(PanelType.HISTORY); aWindow.hidePanel(PanelType.DOWNLOADS); if (leftWindow == aWindow) { removeWindow(leftWindow); if (mFocusedWindow == leftWindow && frontWindow != null) { focusWindow(frontWindow); } } else if (rightWindow == aWindow) { removeWindow(rightWindow); if (mFocusedWindow == rightWindow && frontWindow != null) { 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() && getFrontWindow() != null) { focusWindow(getFrontWindow()); } } boolean empty = getCurrentWindows().isEmpty(); if (empty && isInPrivateMode()) { // Clear private tabs SessionStore.get().destroyPrivateSessions(); // 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(); if (mDelegate != null) { mDelegate.onWindowClosed(); } } public void moveWindowRight(@NonNull WindowWidget aWindow) { WindowWidget frontWindow = getFrontWindow(); WindowWidget leftWindow = getLeftWindow(); WindowWidget rightWindow = getRightWindow(); if (aWindow == leftWindow && frontWindow != null) { placeWindow(leftWindow, WindowPlacement.FRONT); placeWindow(frontWindow, WindowPlacement.LEFT); switchTopBars(leftWindow, frontWindow); } else if (aWindow == frontWindow) { if (rightWindow != null) { placeWindow(rightWindow, WindowPlacement.FRONT); switchTopBars(rightWindow, frontWindow); } else if (leftWindow != null) { placeWindow(leftWindow, WindowPlacement.FRONT); switchTopBars(leftWindow, frontWindow); } placeWindow(frontWindow, WindowPlacement.RIGHT); } updateViews(); if (mDelegate != null) { mDelegate.onWindowsMoved(); } } public void moveWindowLeft(@NonNull WindowWidget aWindow) { WindowWidget frontWindow = getFrontWindow(); WindowWidget leftWindow = getLeftWindow(); WindowWidget rightWindow = getRightWindow(); if (aWindow == rightWindow && frontWindow != null) { placeWindow(rightWindow, WindowPlacement.FRONT); placeWindow(frontWindow, WindowPlacement.RIGHT); switchTopBars(rightWindow, frontWindow); } else if (aWindow == frontWindow) { if (leftWindow != null) { placeWindow(leftWindow, WindowPlacement.FRONT); switchTopBars(leftWindow, frontWindow); } else if (rightWindow != null) { placeWindow(rightWindow, WindowPlacement.FRONT); switchTopBars(rightWindow, frontWindow); } placeWindow(frontWindow, WindowPlacement.LEFT); } updateViews(); if (mDelegate != null) { mDelegate.onWindowsMoved(); } } public void focusWindow(@Nullable WindowWidget aWindow) { if (aWindow != mFocusedWindow) { WindowWidget prev = mFocusedWindow; mFocusedWindow = aWindow; if (prev != null && getCurrentWindows().contains(prev)) { prev.setActiveWindow(false); } mFocusedWindow.setActiveWindow(true); if (mDelegate != null) { mDelegate.onFocusedWindowChanged(mFocusedWindow, prev); } } } public void pauseCompositor() { if (mCompositorPaused) { return; } mCompositorPaused = true; for (WindowWidget window: mRegularWindows) { window.pauseCompositor(); } for (WindowWidget window: mPrivateWindows) { window.pauseCompositor(); } } public void resumeCompositor() { if (!mCompositorPaused) { return; } mCompositorPaused = false; for (WindowWidget window: mRegularWindows) { window.resumeCompositor(); } for (WindowWidget window: mPrivateWindows) { window.resumeCompositor(); } } public void onPause() { mIsPaused = true; saveState(); } public void onResume() { mIsPaused = false; if (mCompositorPaused) { resumeCompositor(); } TelemetryWrapper.resetOpenedWindowsCount(mRegularWindows.size(), false); TelemetryWrapper.resetOpenedWindowsCount(mPrivateWindows.size(), true); GleanMetricsService.resetOpenedWindowsCount(mRegularWindows.size(), false); GleanMetricsService.resetOpenedWindowsCount(mPrivateWindows.size(), true); } public boolean isPaused() { return mIsPaused; } public void onDestroy() { if (mTabsWidget != null && !mTabsWidget.isReleased()) { mTabsWidget.releaseWidget(); mTabsWidget = null; } mDelegate = null; for (WindowWidget window: mRegularWindows) { window.close(); } for (WindowWidget window: mPrivateWindows) { window.close(); } mAccounts.removeAccountListener(mAccountObserver); mServices.setTabReceivedDelegate(null); mWidgetManager.removeConnectivityListener(mConnectivityDelegate); } public boolean isInPrivateMode() { return mPrivateMode; } public boolean isVideoAvailable() { for (WindowWidget window: getCurrentWindows()) { if (window.getSession().isVideoAvailable()) { return true; } } return false; } public void enterImmersiveMode() { if (!isInPrivateMode()) { for (WindowWidget window: mRegularWindows) { if (window != mFocusedWindow) { window.onPause(); } } } else { for (WindowWidget window: mPrivateWindows) { if (window != mFocusedWindow) { window.onPause(); } } } } public void exitImmersiveMode() { if (mIsPaused) { return; } if (!isInPrivateMode()) { for (WindowWidget window: mRegularWindows) { if (window != mFocusedWindow) { window.onResume(); } } } else { for (WindowWidget window: mPrivateWindows) { if (window != mFocusedWindow) { window.onResume(); } } } } public void enterPrivateMode() { if (mPrivateMode) { return; } mPrivateMode = true; if (mFocusedWindow != null) { mRegularWindowPlacement = mFocusedWindow.getWindowPlacement(); } else { mRegularWindowPlacement = WindowPlacement.FRONT; } for (WindowWidget window: mRegularWindows) { setWindowVisible(window, false); } updateViews(); updateCurvedMode(true); for (WindowWidget window: mPrivateWindows) { setWindowVisible(window, true); } if (mPrivateWindows.size() == 0) { WindowWidget window = addWindow(); if (window != null) { window.loadHome(); } } else { focusWindow(getWindowWithPlacement(mPrivateWindowPlacement)); } mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); } public void exitPrivateMode() { if (!mPrivateMode) { return; } mPrivateMode = false; if (mFocusedWindow != null) { mPrivateWindowPlacement = mFocusedWindow.getWindowPlacement(); } else { mPrivateWindowPlacement = WindowPlacement.FRONT; } for (WindowWidget window: mPrivateWindows) { setWindowVisible(window, false); } updateViews(); updateCurvedMode(true); for (WindowWidget window: mRegularWindows) { setWindowVisible(window, true); } WindowWidget window = getWindowWithPlacement(mRegularWindowPlacement); if (window != null) { focusWindow(window); } mWidgetManager.popWorldBrightness(this); } public boolean handleBack() { if (mFocusedWindow == null) { return false; } if (mFocusedWindow.getSession().canGoBack()) { mFocusedWindow.getSession().goBack(); return true; } else if (isInPrivateMode()) { exitPrivateMode(); return true; } return false; } 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); } } public ArrayList<WindowWidget> getCurrentWindows() { return mPrivateMode ? mPrivateWindows : mRegularWindows; } @Nullable private WindowWidget getWindowWithPlacement(WindowPlacement aPlacement) { for (WindowWidget window: getCurrentWindows()) { if (window.getWindowPlacement() == aPlacement) { return window; } } return null; } @Nullable private WindowWidget getFrontWindow() { if (mFullscreenWindow != null) { return mFullscreenWindow; } return getWindowWithPlacement(WindowPlacement.FRONT); } @Nullable private WindowWidget getLeftWindow() { return getWindowWithPlacement(WindowPlacement.LEFT); } @Nullable private WindowWidget getRightWindow() { return getWindowWithPlacement(WindowPlacement.RIGHT); } private void restoreWindows() { if (mIsRestoreEnabled && mWindowsState != null) { for (WindowState windowState : mWindowsState.regularWindowsState) { addRestoredWindow(windowState, null); } WindowWidget windowToFocus = getWindowWithPlacement(mWindowsState.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(); } public void restoreSessions() { if (mIsRestoreEnabled && mWindowsState != null) { ArrayList<Session> restoredSessions = new ArrayList<>(); if (mWindowsState.tabs != null) { mWindowsState.tabs.forEach(state -> { restoredSessions.add(SessionStore.get().createSuspendedSession(state)); GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.PRE_EXISTING); }); } for (WindowState windowState : mWindowsState.regularWindowsState) { WindowWidget targetWindow = getWindowWithPlacement(windowState.placement); if (targetWindow != null) { if (windowState.tabIndex >= 0 && windowState.tabIndex < restoredSessions.size()) { Session defaultSession = targetWindow.getSession(); Session session = restoredSessions.get(windowState.tabIndex); targetWindow.setSession(session, WindowWidget.DEACTIVATE_CURRENT_SESSION); session.setActive(true); // Destroy the default blank session SessionStore.get().destroySession(defaultSession); } else { targetWindow.loadHome(); } } } if (mWindowsState.privateMode) { enterPrivateMode(); } else { exitPrivateMode(); } } if (mAddedTabUri != null) { openNewTab(mAddedTabUri, mAddedTabLocation); mAddedTabUri = null; } mAfterRestore = true; } private void removeWindow(@NonNull WindowWidget aWindow) { BitmapCache.getInstance(mContext).removeBitmap(aWindow.getSession().getId()); mWidgetManager.removeWidget(aWindow); mRegularWindows.remove(aWindow); mPrivateWindows.remove(aWindow); aWindow.removeWindowListener(this); aWindow.close(); updateMaxWindowScales(); updateCurvedMode(true); if (mPrivateMode) { TelemetryWrapper.openWindowsEvent(mPrivateWindows.size() + 1, mPrivateWindows.size(), true); GleanMetricsService.openWindowsEvent(mPrivateWindows.size() + 1, mPrivateWindows.size(), true); } else { TelemetryWrapper.openWindowsEvent(mRegularWindows.size() + 1, mRegularWindows.size(), false); GleanMetricsService.openWindowsEvent(mRegularWindows.size() + 1, mRegularWindows.size(), false); } } private void setWindowVisible(@NonNull WindowWidget aWindow, boolean aVisible) { if (aVisible && (aWindow.getSession() != null) && (aWindow.getSession().getGeckoSession() == null)) { setFirstPaint(aWindow, aWindow.getSession()); } aWindow.setVisible(aVisible); aWindow.getTitleBar().setVisible(aVisible); aWindow.getTopBar().setVisible(aVisible); } private void placeWindow(@NonNull WindowWidget aWindow, WindowPlacement aPosition) { placeWindow(aWindow, aPosition, mStoredCurvedMode || mForcedCurvedMode); } private void placeWindow(@NonNull WindowWidget aWindow, WindowPlacement aPosition, boolean curvedMode) { WidgetPlacement placement = aWindow.getPlacement(); aWindow.setWindowPlacement(aPosition); 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 (curvedMode) { 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 (curvedMode) { 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; } } public void updateCurvedMode(boolean force) { float density = SettingsStore.getInstance(mContext).getCylinderDensity(); boolean storedCurvedMode = density > 0.0f; boolean forcedCurvedMode = getCurrentWindows().size() > 1; if (force) { boolean curved = forcedCurvedMode || storedCurvedMode; for (WindowWidget window : getCurrentWindows()) { placeWindow(window, window.getWindowPlacement(), curved); } updateViews(); mWidgetManager.setCylinderDensity(curved ? SettingsStore.CYLINDER_DENSITY_ENABLED_DEFAULT : density); } else if ((storedCurvedMode != mStoredCurvedMode) || (forcedCurvedMode != mForcedCurvedMode)) { mStoredCurvedMode = storedCurvedMode; mForcedCurvedMode = forcedCurvedMode; boolean curved = mStoredCurvedMode || mForcedCurvedMode; for (WindowWidget window : getCurrentWindows()) { placeWindow(window, window.getWindowPlacement(), curved); } updateViews(); mWidgetManager.setCylinderDensity(curved ? SettingsStore.CYLINDER_DENSITY_ENABLED_DEFAULT : density); } } public int getWindowsCount() { return getCurrentWindows().size(); } public boolean canOpenNewWindow() { return getWindowsCount() < MAX_WINDOWS; } private void switchTopBars(WindowWidget w1, WindowWidget w2) { // Used to fix a minor visual glitch. // See https://github.com/MozillaReality/FirefoxReality/issues/1722 TopBarWidget bar1 = w1.getTopBar(); TopBarWidget bar2 = w2.getTopBar(); w1.setTopBar(bar2); w2.setTopBar(bar1); } 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; } ArrayList<WindowWidget> windows = getCurrentWindows(); for (WindowWidget window: windows) { window.setIsOnlyWindow(windows.size() == 1); } // 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()); mWidgetManager.updateWidget(window.getTitleBar()); } } @NonNull private WindowWidget createWindow(@Nullable Session aSession) { int newWindowId = sIndex++; WindowWidget window; if (aSession != null) { window = new WindowWidget(mContext, newWindowId, aSession); } else { window = new WindowWidget(mContext, newWindowId, mPrivateMode); } window.addWindowListener(this); getCurrentWindows().add(window); window.getTopBar().setDelegate(this); window.getTitleBar().setDelegate(this); if (mPrivateMode) { TelemetryWrapper.openWindowsEvent(mPrivateWindows.size() - 1, mPrivateWindows.size(), true); GleanMetricsService.openWindowsEvent(mPrivateWindows.size() - 1, mPrivateWindows.size(), true); } else { TelemetryWrapper.openWindowsEvent(mRegularWindows.size() - 1, mRegularWindows.size(), false); GleanMetricsService.openWindowsEvent(mRegularWindows.size() - 1, mRegularWindows.size(), false); } mForcedCurvedMode = getCurrentWindows().size() > 1; return window; } public void enterResizeMode() { if (mFullscreenWindow == null) { for (WindowWidget window : getCurrentWindows()) { window.setResizeMode(true); } } } public void exitResizeMode() { if (mFullscreenWindow == null) { for (WindowWidget window : getCurrentWindows()) { window.setResizeMode(false); } } } private AccountObserver mAccountObserver = new AccountObserver() { @Override public void onLoggedOut() { } @Override public void onAuthenticated(@NonNull OAuthAccount oAuthAccount, @NonNull AuthType authType) { if (authType == AuthType.Signin.INSTANCE || authType == AuthType.Signup.INSTANCE) { UIDialog.closeAllDialogs(); Session session = mFocusedWindow.getSession(); addTab(mFocusedWindow, mAccounts.getConnectionSuccessURL()); onTabsClose(new ArrayList<>(Collections.singletonList(session))); switch (mAccounts.getLoginOrigin()) { case BOOKMARKS: mFocusedWindow.getSession().loadUri(UrlUtils.ABOUT_BOOKMARKS); break; case HISTORY: mFocusedWindow.getSession().loadUri(UrlUtils.ABOUT_HISTORY); break; case SETTINGS: mWidgetManager.getTray().showSettingsDialog(FXA); break; } } } @Override public void onProfileUpdated(@NonNull Profile profile) { } @Override public void onAuthenticationProblems() { } }; // Tray Listener @Override public void onBookmarksClicked() { mFocusedWindow.switchPanel(PanelType.BOOKMARKS); } @Override public void onPrivateBrowsingClicked() { if (mPrivateMode) { exitPrivateMode(); } else { enterPrivateMode(); } } @Override public void onAddWindowClicked() { WindowWidget window = addWindow(); if (window != null) { window.loadHome(); } } @Override public void onHistoryClicked() { mFocusedWindow.switchPanel(PanelType.HISTORY); } @Override public void onDownloadsClicked() { mFocusedWindow.switchPanel(PanelType.DOWNLOADS); } @Override public void onTabsClicked() { if (mTabsWidget == null) { mTabsWidget = new TabsWidget(mContext); mTabsWidget.setTabDelegate(this); } if (mFocusedWindow != null) { mTabsWidget.getPlacement().parentHandle = mFocusedWindow.getHandle(); mTabsWidget.attachToWindow(mFocusedWindow); mTabsWidget.show(UIWidget.KEEP_FOCUS); // If we're signed-in, poll for any new device events (e.g. received tabs) // There's no push support right now, so this helps with the perception of speedy tab delivery. ((VRBrowserApplication)mContext.getApplicationContext()).getAccounts().refreshDevicesAsync(); ((VRBrowserApplication)mContext.getApplicationContext()).getAccounts().pollForEventsAsync(); } // Capture active session snapshots when showing the tabs menu for (WindowWidget window: getCurrentWindows()) { window.captureImage(); } } private void setFirstPaint(@NonNull final WindowWidget aWindow, @NonNull final Session aSession) { if (aSession.getGeckoSession() == null) { aWindow.waitForFirstPaint(); } else { // If the new session has a GeckoSession there won't be a first paint event. // So trigger the first paint callback in case the window is grayed out // waiting for the first paint event. aWindow.onFirstContentfulPaint(aSession.getGeckoSession()); } } // 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(); GleanMetricsService.windowsMoveEvent(); moveWindowLeft(window); } } @Override public void onMoveRightClicked(TopBarWidget aWidget) { WindowWidget window = aWidget.getAttachedWindow(); if (window != null) { TelemetryWrapper.windowsMoveEvent(); GleanMetricsService.windowsMoveEvent(); moveWindowRight(window); } } // Title Bar Delegate @Override public void onTitleClicked(@NonNull TitleBarWidget titleBar) { if (titleBar.getAttachedWindow() != null) { focusWindow(titleBar.getAttachedWindow()); } } @Override public void onMediaPlayClicked(@NonNull TitleBarWidget titleBar) { for (WindowWidget window : getCurrentWindows()) { if (window.getTitleBar() == titleBar && window.getSession() != null && window.getSession().getActiveVideo() != null) { window.getSession().getActiveVideo().play(); } } } @Override public void onMediaPauseClicked(@NonNull TitleBarWidget titleBar) { for (WindowWidget window : getCurrentWindows()) { if (window.getTitleBar() == titleBar && window.getSession() != null && window.getSession().getActiveVideo() != null) { window.getSession().getActiveVideo().pause(); } } } private void setFullScreenSize(WindowWidget aWindow) { final float minScale = WidgetPlacement.floatDimension(mContext, R.dimen.window_fullscreen_min_scale); // Set browser fullscreen size float aspect = SettingsStore.getInstance(mContext).getWindowAspect(); Session session = mFocusedWindow.getSession(); if (session == null) { return; } Media media = session.getFullScreenVideo(); if (media != null && media.getWidth() > 0 && media.getHeight() > 0) { aspect = (float)media.getWidth() / (float)media.getHeight(); } float scale = aWindow.getCurrentScale(); // Enforce min fullscreen size. // If current window area is larger only resize if the aspect changes (e.g. media). if (scale < minScale || aspect != aWindow.getCurrentAspect()) { aWindow.resizeByMultiplier(aspect, Math.max(scale, minScale)); } } @Nullable private WindowWidget getWindowWithSession(GeckoSession aSession) { for (WindowWidget window: getCurrentWindows()) { if (window.getSession().getGeckoSession() == aSession) { return window; } } return null; } @Nullable private WindowWidget getWindowWithSession(Session aSession) { for (WindowWidget window: getCurrentWindows()) { if (window.getSession() == aSession) { return window; } } return null; } // WindowWidget.Delegate @Override public void onFocusRequest(@NonNull WindowWidget aWindow) { focusWindow(aWindow); } @Override public void onBorderChanged(@NonNull WindowWidget aWindow) { if (mDelegate != null) { mDelegate.onWindowBorderChanged(aWindow); } } @Override public void onVideoAvailabilityChanged(@NonNull WindowWidget aWindow) { if (mDelegate != null) { mDelegate.onWindowVideoAvailabilityChanged(aWindow); } } @Override public void onFullScreen(@NonNull WindowWidget aWindow, boolean aFullScreen) { if (aFullScreen) { mFullscreenWindow = aWindow; aWindow.saveBeforeFullscreenPlacement(); // Do not depend on how many windows are opened to select flat/curved when entering fullscreen. boolean fullscreenCurved = SettingsStore.getInstance(mContext).isCurvedModeEnabled() && (mStoredCurvedMode || mForcedCurvedMode); aWindow.getPlacement().cylinder = fullscreenCurved; setFullScreenSize(aWindow); placeWindow(aWindow, WindowPlacement.FRONT, fullscreenCurved); focusWindow(aWindow); for (WindowWidget win: getCurrentWindows()) { setWindowVisible(win, win == mFullscreenWindow); } updateMaxWindowScales(); updateViews(); } else if (mFullscreenWindow != null) { aWindow.restoreBeforeFullscreenPlacement(); mFullscreenWindow = null; for (WindowWidget win : getCurrentWindows()) { setWindowVisible(win, true); } updateMaxWindowScales(); updateViews(); } } @Override public void onTabSelect(Session aTab) { if (mFocusedWindow.getSession() != aTab) { GleanMetricsService.Tabs.activatedEvent(); } WindowWidget targetWindow = mFocusedWindow; WindowWidget windowToMove = getWindowWithSession(aTab); if (windowToMove != null && windowToMove != targetWindow) { // Move session between windows Session moveFrom = windowToMove.getSession(); Session moveTo = targetWindow.getSession(); moveFrom.surfaceDestroyed(); moveTo.surfaceDestroyed(); windowToMove.setSession(moveTo, WindowWidget.SESSION_DO_NOT_RELEASE_DISPLAY, WindowWidget.LEAVE_CURRENT_SESSION_ACTIVE); targetWindow.setSession(moveFrom, WindowWidget.SESSION_DO_NOT_RELEASE_DISPLAY, WindowWidget.LEAVE_CURRENT_SESSION_ACTIVE); windowToMove.setActiveWindow(false); targetWindow.setActiveWindow(true); } else { setFirstPaint(targetWindow, aTab); targetWindow.setSession(aTab, WindowWidget.DEACTIVATE_CURRENT_SESSION); } } public void addTab(WindowWidget targetWindow) { addTab(targetWindow, null); } public void openNewTabAfterRestore(@NonNull String aUri, @NewTabLocation int aLocation) { if (mAfterRestore) { openNewTab(aUri, aLocation); } else { mAddedTabUri = aUri; mAddedTabLocation = aLocation; } } private void openNewTab(@NonNull String aUri, @NewTabLocation int aLocation) { if (aLocation == OPEN_IN_NEW_WINDOW) { WindowWidget newWindow = addWindow(); if ((newWindow != null) && (newWindow.getSession() != null)) { newWindow.getSession().loadUri(aUri); } } else if (mFocusedWindow != null) { if (aLocation == OPEN_IN_FOREGROUND) { addTab(mFocusedWindow, aUri); } else if (aLocation == OPEN_IN_BACKGROUND) { addBackgroundTab(mFocusedWindow, aUri); } } } public void addTab(@NonNull WindowWidget targetWindow, @Nullable String aUri) { Session session = SessionStore.get().createSuspendedSession(aUri, targetWindow.getSession().isPrivateMode()); setFirstPaint(targetWindow, session); targetWindow.setSession(session, WindowWidget.DEACTIVATE_CURRENT_SESSION); if (aUri == null || aUri.isEmpty()) { session.loadHomePage(); } else { session.loadUri(aUri); } } public void addBackgroundTab(WindowWidget targetWindow, String aUri) { Session session = SessionStore.get().createSuspendedSession(aUri, targetWindow.getSession().isPrivateMode()); session.updateLastUse(); mFocusedWindow.getSession().updateLastUse(); showTabAddedNotification(); } @Override public void onTabAdd() { addTab(mFocusedWindow, null); GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.TABS_DIALOG); } @Override public void onTabsClose(ArrayList<Session> aTabs) { WindowWidget targetWindow = mFocusedWindow; // Prepare available tabs to choose from ArrayList<Session> available = SessionStore.get().getSortedSessions(mPrivateMode); available.removeAll(aTabs); available.removeIf(session -> getWindowWithSession(session) != null); // Sort windows by priority to take an available tab WindowWidget front = getFrontWindow(); ArrayList<WindowWidget> windows = new ArrayList<>(getCurrentWindows()); windows.sort((w1, w2) -> { // Max priority for the target window if (w1 == targetWindow) { return -1; } if (w2 == targetWindow) { return 1; } // Front window has next max priority if (w1 == front) { return -1; } if (w2 == front) { return 1; } return 0; }); // Take tabs for each window for (WindowWidget window: windows) { if (!aTabs.contains(window.getSession())) { // Window already contains a no closed tab continue; } if (available.size() > 0) { // Window contains a closed tab and we have a tab available from the list Session tab = available.get(0); if (tab != null) { setFirstPaint(window, tab); window.setSession(tab, WindowWidget.LEAVE_CURRENT_SESSION_ACTIVE); } available.remove(0); } else { // We don't have more tabs available for the front window, load home. addTab(window, null); } } BitmapCache cache = BitmapCache.getInstance(mContext); for (Session session: aTabs) { cache.removeBitmap(session.getId()); SessionStore.get().destroySession(session); } } @Override public void onTabsReceived(@NonNull List<TabData> aTabs) { WindowWidget targetWindow = mFocusedWindow; boolean fullscreen = targetWindow.getSession().isInFullScreen(); for (int i = aTabs.size() - 1; i >= 0; --i) { Session session = SessionStore.get().createSession(targetWindow.getSession().isPrivateMode()); // Cache the provided data to avoid delays if the tabs are loaded at the same time the // tabs panel is shown. session.getSessionState().mTitle = aTabs.get(i).getTitle(); session.getSessionState().mUri = aTabs.get(i).getUrl(); session.loadUri(aTabs.get(i).getUrl()); session.updateLastUse(); GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.RECEIVED); if (i == 0 && !fullscreen) { // Set the first received tab of the list the current one. targetWindow.setSession(session, WindowWidget.DEACTIVATE_CURRENT_SESSION); } } if (!fullscreen) { showTabAddedNotification(); } if (mTabsWidget != null && mTabsWidget.isVisible()) { mTabsWidget.refreshTabs(); } } private ConnectivityReceiver.Delegate mConnectivityDelegate = connected -> { if (mNoInternetDialog == null) { mNoInternetDialog = new PromptDialogWidget(mContext); mNoInternetDialog.setButtons(new int[] { R.string.ok_button }); mNoInternetDialog.setCheckboxVisible(false); mNoInternetDialog.setDescriptionVisible(false); mNoInternetDialog.setTitle(R.string.no_internet_title); mNoInternetDialog.setBody(R.string.no_internet_message); mNoInternetDialog.setButtonsDelegate((index, isChecked) -> { mNoInternetDialog.hide(UIWidget.REMOVE_WIDGET); mNoInternetDialog.releaseWidget(); mNoInternetDialog = null; }); } if (!connected && !mNoInternetDialog.isVisible()) { mNoInternetDialog.show(UIWidget.REQUEST_FOCUS); } else if (connected && mNoInternetDialog.isVisible()) { mNoInternetDialog.hide(UIWidget.REMOVE_WIDGET); mNoInternetDialog.releaseWidget(); mNoInternetDialog = null; } }; public void showTabAddedNotification() { if (mFocusedWindow.isFullScreen()) { mWidgetManager.getNavigationBar().showTabAddedNotification(); } else { if (mWidgetManager.getTray().isVisible()) { mWidgetManager.getTray().showTabAddedNotification(); } else { NotificationManager.Notification notification = new NotificationManager.Builder(mFocusedWindow) .withString(R.string.tab_added_notification) .withZTranslation(25.0f) .withCurved(true).build(); NotificationManager.show(TAB_ADDED_NOTIFICATION_ID, notification); } } } public void showTabSentNotification() { if (mFocusedWindow.isFullScreen()) { mWidgetManager.getNavigationBar().showTabSentNotification(); } else { if (mWidgetManager.getTray().isVisible()) { mWidgetManager.getTray().showTabSentNotification(); } else { NotificationManager.Notification notification = new NotificationManager.Builder(mFocusedWindow) .withString(R.string.tab_sent_notification) .withZTranslation(25.0f) .withCurved(true).build(); NotificationManager.show(TAB_SENT_NOTIFICATION_ID, notification); } } } public void showBookmarkAddedNotification() { if (mFocusedWindow.isFullScreen()) { mWidgetManager.getNavigationBar().showBookmarkAddedNotification(); } else { if (mWidgetManager.getTray().isVisible()) { mWidgetManager.getTray().showBookmarkAddedNotification(); } else { NotificationManager.Notification notification = new NotificationManager.Builder(mFocusedWindow) .withString(R.string.bookmarks_saved_notification) .withZTranslation(25.0f) .withCurved(true).build(); NotificationManager.show(BOOKMARK_ADDED_NOTIFICATION_ID, notification); } } } }