diff --git a/.taskcluster.yml b/.taskcluster.yml index bbdee725c..7fe7e771a 100644 --- a/.taskcluster.yml +++ b/.taskcluster.yml @@ -43,6 +43,7 @@ tasks: && rm -rf gvr-android-sdk && git clone https://github.com/MozillaReality/FirefoxReality-gvr-android-sdk.git gvr-android-sdk && git submodule update && ./gradlew --no-daemon --console=plain clean `python tools/taskcluster/build_targets.py =all+googlevr+noapi` + && ./gradlew app:testNoapiArm64DebugUnitTest metadata: name: Firefox Reality for Android - Build - Pull Request description: Building Firefox Reality for Android (via Gradle) - triggered by a pull request. diff --git a/app/build.gradle b/app/build.gradle index fe5bb6734..98b837a68 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,6 +6,7 @@ apply plugin: 'com.android.application' apply from: "$project.rootDir/tools/gradle/versionCode.gradle" apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" def getGitHash = { -> def stdout = new ByteArrayOutputStream() @@ -37,10 +38,9 @@ def getUseDebugSigningOnRelease = { -> return false } -// Generate markdown docs for the collected metrics. +// Glean: Generate markdown docs for the collected metrics. ext.gleanGenerateMarkdownDocs = true ext.gleanDocsDirectory = "$rootDir/docs" -apply from: 'https://github.com/mozilla-mobile/android-components/raw/v' + deps.android_components_version + '/components/service/glean/scripts/sdk_generator.gradle' android { compileSdkVersion build_versions.target_sdk @@ -49,7 +49,7 @@ android { minSdkVersion build_versions.min_sdk targetSdkVersion build_versions.target_sdk versionCode 1 - versionName "7" + versionName "8" buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\"" buildConfigField "Boolean", "DISABLE_CRASH_RESTART", getCrashRestartDisabled() testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -154,7 +154,7 @@ android { arguments "-DVR_SDK_LIB=oculusvr-lib", "-DVR_SDK_EXTRA_LIB=ovrplatform-lib", "-DOCULUSVR=ON" } } - manifestPlaceholders = [ headtrackingRequired:"true", permissionToRemove:"android.permission.RECEIVE_BOOT_COMPLETED" ] + manifestPlaceholders = [ headtrackingRequired:"false", permissionToRemove:"android.permission.RECEIVE_BOOT_COMPLETED" ] } oculusvr3dofStore { @@ -414,6 +414,10 @@ android { ] } } + + testOptions { + unitTests.includeAndroidResources = true + } } configurations { @@ -461,6 +465,19 @@ dependencies { implementation deps.android_components.support_rustlog implementation deps.android_components.support_rusthttp implementation deps.android_components.glean + implementation deps.app_services.rustlog + + // For production builds, the native code for all `org.mozilla.appservices` + // dependencies gets compiled together into a single "megazord" build, and + // different megazords are published for different subsets of features. Ref + // https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html + // For now we can jut use the one that's specifically designed for Fenix. + implementation deps.app_services.megazord + modules { + module('org.mozilla.appservices:full-megazord') { + replacedBy('org.mozilla.appservices:fenix-megazord', 'prefer the fenix megazord, to reduce final application size') + } + } // TODO this should not be necessary at all, see Services.kt implementation deps.work.runtime @@ -480,9 +497,16 @@ dependencies { implementation deps.disklrucache.disklrucache // Testing - testImplementation deps.junit androidTestImplementation deps.atsl.runner androidTestImplementation deps.espresso.core + testImplementation deps.junit + testImplementation deps.atsl.core + testImplementation deps.robolectric + testImplementation deps.app_services.megazord_forUnitTests + testImplementation deps.app_services.rustlog + testImplementation deps.android_components.support_test + testImplementation deps.telemetry.glean_unittests + testImplementation deps.work.testing // Daydream googlevrImplementation deps.google_vr.sdk_base diff --git a/app/metrics.yaml b/app/metrics.yaml index 3bf7de5f6..d1083e770 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -16,10 +16,225 @@ distribution: - baseline - events - metrics + - session_end bugs: - https://github.com/MozillaReality/FirefoxReality/issues/1420 data_reviews: - https://github.com/MozillaReality/FirefoxReality/pull/1854#issuecomment-546214568 notification_emails: + - fxr-telemetry@mozilla.com - dmu@mozilla.com expires: "2020-05-01" + +url: + domains: + type: counter + send_in_pings: + - session_end + description: > + Counting how many domains are visited in a session. + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/2230 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2241#issuecomment-557740258 + notification_emails: + - fxr-telemetry@mozilla.com + - dmu@mozilla.com + expires: "2020-05-01" + visits: + type: counter + send_in_pings: + - session_end + description: > + Counting how many URL links are visited in a session. + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/2230 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2241#issuecomment-557740258 + notification_emails: + - fxr-telemetry@mozilla.com + - dmu@mozilla.com + expires: "2020-05-01" + query_type: + type: labeled_counter + description: > + Counting how many URLs are visited in a day, by input method. + labels: + - type_link + - type_query + - voice_query + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/2230 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2241#issuecomment-557740258 + notification_emails: + - fxr-telemetry@mozilla.com + - dmu@mozilla.com + expires: "2020-05-01" + +searches: + counts: + type: labeled_counter + description: > + Counting how many searches are queried in a specific search engine. The search engine `identifier`s are used as keys for this metric. + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/2230 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2241#issuecomment-557740258 + notification_emails: + - fxr-telemetry@mozilla.com + - dmu@mozilla.com + expires: "2020-05-01" + +tabs: + opened: + type: labeled_counter + description: > + Number of tabs opened during a session + send_in_pings: + - session_end + labels: + - context_menu + - tabs_dialog + - bookmarks + - history + - fxa_login + - received + - pre_existing + - browser + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/1609 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2327#issuecomment-559103837 + notification_emails: + - fxr-telemetry@mozilla.com + - manmartin@mozilla.com + expires: "2020-05-01" + activated: + type: counter + description: > + Number of tabs activated during a session + send_in_pings: + - session_end + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/1609 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2327#issuecomment-559103837 + notification_emails: + - fxr-telemetry@mozilla.com + - manmartin@mozilla.com + expires: "2020-05-01" + +firefox_account: + sign_in: + type: event + description: > + The user starts the sign in flow + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/1610 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2327#issuecomment-559103837 + notification_emails: + - fxr-telemetry@mozilla.com + - manmartin@mozilla.com + expires: "2020-05-01" + sign_in_result: + type: event + description: > + The user finishes the sign in flow + extra_keys: + state: + description: "The result of the sign in flow. True in case of success, false in case of error" + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/1610 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2327#issuecomment-559103837 + notification_emails: + - fxr-telemetry@mozilla.com + - manmartin@mozilla.com + expires: "2020-05-01" + sign_out: + type: event + description: > + A user pressed the sign out button on the sync account page and was successfully signed out of FxA + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/1610 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2327#issuecomment-559103837 + notification_emails: + - fxr-telemetry@mozilla.com + - manmartin@mozilla.com + expires: "2020-05-01" + bookmarks_sync_status: + type: boolean + lifetime: application + description: > + Bookmarks sync engine status. True means that the bookmarks sync status is enabled, false otherwise. + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/1610 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2327#issuecomment-559103837 + notification_emails: + - fxr-telemetry@mozilla.com + - manmartin@mozilla.com + expires: "2020-05-01" + history_sync_status: + type: boolean + lifetime: application + description: > + History sync engine status. True means that the history sync status is enabled, false otherwise. + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/1610 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2327#issuecomment-559103837 + notification_emails: + - fxr-telemetry@mozilla.com + - manmartin@mozilla.com + expires: "2020-05-01" + tab_sent: + type: counter + description: > + Number of tabs successfully sent per day + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/1610 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2327#issuecomment-559103837 + notification_emails: + - fxr-telemetry@mozilla.com + - manmartin@mozilla.com + expires: "2020-05-01" + received_tab: + type: labeled_counter + description: > + Number of received tabs per day + labels: + - desktop + - mobile + - tablet + - tv + - vr + - unknown + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/1610 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2327#issuecomment-559103837 + notification_emails: + - fxr-telemetry@mozilla.com + - manmartin@mozilla.com + expires: "2020-05-01" + +control: + open_new_window: + type: counter + description: > + Counting how many general windows are opened in a session. + send_in_pings: + - session_end + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/2347 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2348#issuecomment-564736919 + notification_emails: + - fxr-telemetry@mozilla.com + - dmu@mozilla.com + expires: "2020-05-01" \ No newline at end of file diff --git a/app/pings.yaml b/app/pings.yaml new file mode 100644 index 000000000..67ee75831 --- /dev/null +++ b/app/pings.yaml @@ -0,0 +1,18 @@ +# 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/. + +$schema: moz://mozilla.org/schemas/glean/pings/1-0-0 + +session_end: + description: > + This ping is sent at the end of a session (when Firefox Reality switches to the background). + We usually send search and UI control metrics at the end of a session. + include_client_id: true + bugs: + - https://github.com/MozillaReality/FirefoxReality/issues/2230 + data_reviews: + - https://github.com/MozillaReality/FirefoxReality/pull/2241#issuecomment-557740258 + notification_emails: + - fxr-telemetry@mozilla.com + - dmu@mozilla.com diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 7b578021b..357020235 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -60,13 +60,6 @@ -keep class com.htc.** {*;} -keep class com.qualcomm.** {*;} -# -------------------------------------------------------------------- -# Keep rules for mozillaspeechlibrary dependency -# -------------------------------------------------------------------- --keep class cz.msebera.android.httpclient.** {*;} --keep class com.loopj.android.http.** {*;} --keep class com.github.axet.opusjni.Opus {*;} - -dontwarn ** -target 1.7 -dontusemixedcaseclassnames diff --git a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java index 75975fd87..707fb33e7 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java @@ -36,9 +36,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.mozilla.gecko.util.ThreadUtils; -import org.mozilla.geckoview.CrashReporter; -import org.mozilla.geckoview.GeckoResult; import org.mozilla.geckoview.GeckoRuntime; import org.mozilla.geckoview.GeckoSession; import org.mozilla.geckoview.GeckoVRManager; @@ -53,22 +50,24 @@ import org.mozilla.vrbrowser.geolocation.GeolocationWrapper; import org.mozilla.vrbrowser.input.MotionEventGenerator; import org.mozilla.vrbrowser.search.SearchEngineWrapper; +import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.OffscreenDisplay; 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.TrayWidget; +import org.mozilla.vrbrowser.ui.widgets.UISurfaceTextureRenderer; import org.mozilla.vrbrowser.ui.widgets.UIWidget; -import org.mozilla.vrbrowser.ui.widgets.menus.VideoProjectionMenuWidget; import org.mozilla.vrbrowser.ui.widgets.Widget; 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.ui.widgets.dialogs.PromptDialogWidget; import org.mozilla.vrbrowser.ui.widgets.dialogs.WhatsNewWidget; -import org.mozilla.vrbrowser.ui.widgets.prompts.ConfirmPromptWidget; +import org.mozilla.vrbrowser.ui.widgets.menus.VideoProjectionMenuWidget; import org.mozilla.vrbrowser.utils.BitmapCache; import org.mozilla.vrbrowser.utils.ConnectivityReceiver; import org.mozilla.vrbrowser.utils.ConnectivityReceiver.Delegate; @@ -77,13 +76,6 @@ import org.mozilla.vrbrowser.utils.ServoUtils; import org.mozilla.vrbrowser.utils.SystemUtils; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -125,7 +117,6 @@ public void run() { static final int GestureSwipeRight = 1; static final int SwipeDelay = 1000; // milliseconds static final long RESET_CRASH_COUNT_DELAY = 5000; - static final String CRASH_STATS_URL = "https://crash-stats.mozilla.com/report/index/"; static final String LOGTAG = SystemUtils.createLogtag(VRBrowserActivity.class); HashMap mWidgets; @@ -210,7 +201,7 @@ protected void onCreate(Bundle savedInstanceState) { // Set a global exception handler as soon as possible GlobalExceptionHandler.register(this.getApplicationContext()); - LocaleUtils.init(this); + LocaleUtils.init(); if (DeviceType.isOculusBuild()) { workaroundGeckoSigAction(); @@ -223,11 +214,12 @@ protected void onCreate(Bundle savedInstanceState) { SessionStore.get().setContext(this, extras); SessionStore.get().initializeServices(); SessionStore.get().initializeStores(this); + SessionStore.get().setLocales(LocaleUtils.getPreferredLocales(this)); // Create broadcast receiver for getting crash messages from crash process IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(CrashReporterService.CRASH_ACTION); - registerReceiver(mCrashReceiver, intentFilter, getString(R.string.app_permission_name), null); + registerReceiver(mCrashReceiver, intentFilter, BuildConfig.APPLICATION_ID + "." + getString(R.string.app_permission_name), null); mLastGesture = NoGesture; super.onCreate(savedInstanceState); @@ -280,13 +272,15 @@ protected void onCreate(Bundle savedInstanceState) { } protected void initializeWidgets() { + UISurfaceTextureRenderer.setUseHardwareAcceleration(SettingsStore.getInstance(getBaseContext()).isUIHardwareAccelerationEnabled()); + UISurfaceTextureRenderer.setRenderActive(true); mWindows = new Windows(this); mWindows.setDelegate(new Windows.Delegate() { @Override public void onFocusedWindowChanged(@NonNull WindowWidget aFocusedWindow, @Nullable WindowWidget aPrevFocusedWindow) { attachToWindow(aFocusedWindow, aPrevFocusedWindow); mTray.setAddWindowVisible(mWindows.canOpenNewWindow()); - mNavigationBar.hidePopUpsBlockedNotification(); + mNavigationBar.hideNotifications(); } @Override public void onWindowBorderChanged(@NonNull WindowWidget aChangeWindow) { @@ -295,14 +289,14 @@ public void onWindowBorderChanged(@NonNull WindowWidget aChangeWindow) { @Override public void onWindowsMoved() { - mNavigationBar.hidePopUpsBlockedNotification(); + mNavigationBar.hideNotifications(); updateWidget(mTray); } @Override public void onWindowClosed() { mTray.setAddWindowVisible(mWindows.canOpenNewWindow()); - mNavigationBar.hidePopUpsBlockedNotification(); + mNavigationBar.hideNotifications(); updateWidget(mTray); } @@ -341,22 +335,10 @@ public void onWindowVideoAvailabilityChanged(@NonNull WindowWidget aWindow) { addWidgets(Arrays.asList(mRootWidget, mNavigationBar, mKeyboard, mTray)); // Show the what's upp dialog if we haven't showed it yet and this is v6. - if (!SettingsStore.getInstance(this).isWhatsNewDisplayed() && BuildConfig.VERSION_NAME.equals("6")) { + if (!SettingsStore.getInstance(this).isWhatsNewDisplayed()) { final WhatsNewWidget whatsNew = new WhatsNewWidget(this); whatsNew.setLoginOrigin(Accounts.LoginOrigin.NONE); whatsNew.getPlacement().parentHandle = mWindows.getFocusedWindow().getHandle(); - whatsNew.setStartBrowsingCallback(() -> { - whatsNew.hide(UIWidget.REMOVE_WIDGET); - whatsNew.releaseWidget(); - }); - whatsNew.setSignInCallback(() -> { - whatsNew.hide(UIWidget.REMOVE_WIDGET); - whatsNew.releaseWidget(); - }); - whatsNew.setDelegate(() -> { - whatsNew.hide(UIWidget.REMOVE_WIDGET); - whatsNew.releaseWidget(); - }); whatsNew.show(UIWidget.REQUEST_FOCUS); } } @@ -379,6 +361,7 @@ protected void onStart() { SettingsStore.getInstance(getBaseContext()).setPid(Process.myPid()); super.onStart(); TelemetryWrapper.start(); + UISurfaceTextureRenderer.setRenderActive(true); } @Override @@ -386,11 +369,9 @@ protected void onStop() { SettingsStore.getInstance(getBaseContext()).setPid(0); super.onStop(); - if (SettingsStore.getInstance(this).getCylinderDensity() > 0.0f) { - TelemetryWrapper.queueCurvedModeActiveEvent(); - } - TelemetryWrapper.stop(); + GleanMetricsService.sessionStop(); + UISurfaceTextureRenderer.setRenderActive(false); } @Override @@ -483,9 +464,8 @@ protected void onNewIntent(final Intent intent) { setIntent(intent); final String action = intent.getAction(); if (Intent.ACTION_VIEW.equals(action)) { - if (intent.getData() != null) { - loadFromIntent(intent); - } + loadFromIntent(intent); + } else if (GeckoRuntime.ACTION_CRASHED.equals(intent.getAction())) { Log.e(LOGTAG, "Restarted after a crash"); } @@ -504,33 +484,80 @@ void loadFromIntent(final Intent intent) { } Uri uri = intent.getData(); - if (uri == null && intent.getExtras() != null && intent.getExtras().containsKey("url")) { - uri = Uri.parse(intent.getExtras().getString("url")); - } - Session activeSession = SessionStore.get().getActiveSession(); + boolean openInWindow = false; + boolean openInTab = false; + boolean openInBackground = false; Bundle extras = intent.getExtras(); - if (extras != null && extras.containsKey("homepage")) { - Uri homepageUri = Uri.parse(extras.getString("homepage")); - SettingsStore.getInstance(this).setHomepage(homepageUri.toString()); - } - if (extras != null && extras.containsKey("e10s")) { - boolean wasEnabled = SettingsStore.getInstance(this).isMultiprocessEnabled(); - boolean enabled = extras.getBoolean("e10s", wasEnabled); - if (wasEnabled != enabled) { - SettingsStore.getInstance(this).setMultiprocessEnabled(enabled); - SessionStore.get().resetMultiprocess(); + if (extras != null) { + // If there is no data uri and there is a url parameter we get that + if (uri == null && extras.containsKey("url")) { + uri = Uri.parse(intent.getExtras().getString("url")); + } + + // Overwrite the stored homepage + if (extras.containsKey("homepage")) { + Uri homepageUri = Uri.parse(extras.getString("homepage")); + SettingsStore.getInstance(this).setHomepage(homepageUri.toString()); + } + + // Enable/Disbale e10s + if (extras.containsKey("e10s")) { + boolean wasEnabled = SettingsStore.getInstance(this).isMultiprocessEnabled(); + boolean enabled = extras.getBoolean("e10s", wasEnabled); + if (wasEnabled != enabled) { + SettingsStore.getInstance(this).setMultiprocessEnabled(enabled); + SessionStore.get().resetMultiprocess(); + } + } + + // Open the provided URL in a new tab, if there is no URL provided we just open the homepage + if (extras.containsKey("create_new_tab")) { + openInTab = extras.getBoolean("create_new_tab", false); + if (uri == null) { + uri = Uri.parse(SettingsStore.getInstance(this).getHomepage()); + } + } + + // Open the tab in background/foreground, if there is no URL provided we just open the homepage + if (extras.containsKey("background")) { + openInBackground = extras.getBoolean("background", false); + if (uri == null) { + uri = Uri.parse(SettingsStore.getInstance(this).getHomepage()); + } + } + + // Open the provided URL in a new window, if there is no URL provided we just open the homepage + if (extras.containsKey("create_new_window")) { + openInWindow = extras.getBoolean("create_new_window", false); + if (uri == null) { + uri = Uri.parse(SettingsStore.getInstance(this).getHomepage()); + } } } - if (activeSession != null) { - if (uri != null) { - Log.d(LOGTAG, "Loading URI from intent: " + uri.toString()); - activeSession.loadUri(uri.toString()); + // If there is a URI we open it + if (uri != null) { + Log.d(LOGTAG, "Loading URI from intent: " + uri.toString()); + + if (openInWindow) { + openNewWindow(uri.toString()); + + } else if (openInTab) { + if (openInBackground) { + openNewTab(uri.toString()); + + } else { + openNewTabForeground(uri.toString()); + } + } else { - mWindows.getFocusedWindow().loadHomeIfNotRestored(); + SessionStore.get().getActiveSession().loadUri(uri.toString()); } + + } else { + mWindows.getFocusedWindow().loadHomeIfNotRestored(); } } @@ -547,27 +574,12 @@ private void checkForCrash() { } boolean isCrashReportingEnabled = SettingsStore.getInstance(this).isCrashReportingEnabled(); if (isCrashReportingEnabled) { - postCrashFiles(files); + SystemUtils.postCrashFiles(this, files); + } else { if (mCrashDialog == null) { - mCrashDialog = new CrashDialogWidget(this); + mCrashDialog = new CrashDialogWidget(this, files); } - mCrashDialog.setCrashDialogDelegate( - new CrashDialogWidget.CrashDialogDelegate() { - @Override - public void onSendData() { - postCrashFiles(files); - } - - @Override - public void onDoNotSendData() { - for (String file : files) { - Log.e(LOGTAG, "Deleting crashfile: " + file); - getBaseContext().deleteFile(file); - } - } - } - ); mCrashDialog.show(UIWidget.REQUEST_FOCUS); } } @@ -582,65 +594,16 @@ private void handleContentCrashIntent(@NonNull final Intent intent) { boolean isCrashReportingEnabled = SettingsStore.getInstance(this).isCrashReportingEnabled(); if (isCrashReportingEnabled) { - postCrashFiles(dumpFile, extraFile); + SystemUtils.postCrashFiles(this, dumpFile, extraFile); + } else { if (mCrashDialog == null) { - mCrashDialog = new CrashDialogWidget(this); + mCrashDialog = new CrashDialogWidget(this, dumpFile, extraFile); } - mCrashDialog.setCrashDialogDelegate(() -> postCrashFiles(dumpFile, extraFile)); mCrashDialog.show(UIWidget.REQUEST_FOCUS); } } - private void sendCrashFiles(@NonNull final String aDumpFile, @NonNull final String aExtraFile) { - try { - GeckoResult result = CrashReporter.sendCrashReport(VRBrowserActivity.this, new File(aDumpFile), new File(aExtraFile), getString(R.string.crash_app_name)); - - result.accept(crashID -> { - Log.e(LOGTAG, "Submitted crash report id: " + crashID); - Log.e(LOGTAG, "Report available at: " + CRASH_STATS_URL + crashID); - }, ex -> { - Log.e(LOGTAG, "Failed to submit crash report: " + (ex != null ? ex.getMessage() : "Exception is NULL")); - }); - } catch (IOException | URISyntaxException e) { - Log.e(LOGTAG, "Failed to send crash report: " + e.toString()); - } - } - - private void postCrashFiles(@NonNull final String aDumpFile, @NonNull final String aExtraFile) { - ThreadUtils.postToBackgroundThread(() -> { - sendCrashFiles(aDumpFile, aExtraFile); - }); - } - - private void postCrashFiles(final ArrayList aFiles) { - ThreadUtils.postToBackgroundThread(() -> { - for (String file: aFiles) { - try { - ArrayList list = new ArrayList<>(2); - try (FileInputStream in = getBaseContext().openFileInput(file)) { - try(BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { - String line; - while((line = br.readLine()) != null) { - list.add(line); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - if (list.size() < 2) { - Log.e(LOGTAG, "Failed read crash dump file names from: " + file); - return; - } - sendCrashFiles(list.get(0), list.get(1)); - } finally { - Log.d(LOGTAG,"Removing crash file: " + file); - getBaseContext().deleteFile(file); - } - } - }); - } - @Override public void onTrimMemory(int level) { @@ -689,6 +652,10 @@ public boolean dispatchKeyEvent(KeyEvent event) { } final int keyCode = event.getKeyCode(); if (DeviceType.isOculusBuild()) { + if (event.getKeyCode() == KeyEvent.KEYCODE_SEARCH) { + // Eat search key, otherwise it causes a crash on Oculus + return true; + } int action = event.getAction(); if (action != KeyEvent.ACTION_DOWN) { return super.dispatchKeyEvent(event); @@ -951,6 +918,7 @@ void pauseGeckoViewCompositor() { mIsPresentingImmersive = true; mWindows.enterImmersiveMode(); TelemetryWrapper.startImmersive(); + GleanMetricsService.startImmersive(); PauseCompositorRunnable runnable = new PauseCompositorRunnable(); synchronized (this) { @@ -977,6 +945,7 @@ void resumeGeckoViewCompositor() { resetUIYaw(); TelemetryWrapper.uploadImmersiveToHistogram(); + GleanMetricsService.stopImmersive(); Handler handler = new Handler(Looper.getMainLooper()); handler.postDelayed(() -> { mWindows.resumeCompositor(); @@ -1060,7 +1029,7 @@ private void haltActivity(final int aReason) { mWindows.getFocusedWindow().showAlert( getString(R.string.not_entitled_title), getString(R.string.not_entitled_message, getString(R.string.app_name)), - () -> VRBrowserActivity.this.finish()); + index -> finish()); } }); } @@ -1086,18 +1055,10 @@ private void handlePoorPerformance() { } window.getSession().loadHomePage(); final String[] buttons = {getString(R.string.ok_button), getString(R.string.performance_unblock_page)}; - window.showButtonPrompt(getString(R.string.performance_title), getString(R.string.performance_message), buttons, new ConfirmPromptWidget.ConfirmPromptDelegate() { - @Override - public void confirm(int index) { - if (index == GeckoSession.PromptDelegate.ButtonPrompt.Type.NEGATIVE) { - mPoorPerformanceWhiteList.add(originalUri); - window.getSession().loadUri(originalUri); - } - } - - @Override - public void dismiss() { - + window.showConfirmPrompt(getString(R.string.performance_title), getString(R.string.performance_message), buttons, index -> { + if (index == PromptDialogWidget.NEGATIVE) { + mPoorPerformanceWhiteList.add(originalUri); + window.getSession().loadUri(originalUri); } }); }); @@ -1509,6 +1470,11 @@ public TrayWidget getTray() { return mTray; } + @Override + public void saveState() { + mWindows.saveState(); + } + private native void addWidgetNative(int aHandle, WidgetPlacement aPlacement); private native void updateWidgetNative(int aHandle, WidgetPlacement aPlacement); private native void updateVisibleWidgetsNative(); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/Accounts.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/Accounts.kt index e8a46ba94..04d785140 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/Accounts.kt +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/Accounts.kt @@ -6,6 +6,8 @@ package org.mozilla.vrbrowser.browser import android.content.Context +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable import android.os.Handler import android.os.Looper import android.util.Log @@ -21,12 +23,16 @@ import mozilla.components.service.fxa.manager.SyncEnginesStorage import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.service.fxa.sync.SyncStatusObserver import mozilla.components.service.fxa.sync.getLastSynced -import mozilla.components.support.base.log.logger.Logger +import org.mozilla.vrbrowser.R import org.mozilla.vrbrowser.VRBrowserApplication +import org.mozilla.vrbrowser.telemetry.GleanMetricsService +import org.mozilla.vrbrowser.utils.BitmapCache import org.mozilla.vrbrowser.utils.SystemUtils +import org.mozilla.vrbrowser.utils.ViewUtils +import java.net.URL import java.util.concurrent.CompletableFuture -import java.util.concurrent.ExecutionException -import java.util.concurrent.Executors + +const val PROFILE_PICTURE_TAG = "fxa_profile_picture" class Accounts constructor(val context: Context) { @@ -46,6 +52,7 @@ class Accounts constructor(val context: Context) { NONE } + var profilePicture: BitmapDrawable? = loadDefaultProfilePicture() var loginOrigin: LoginOrigin = LoginOrigin.NONE var accountStatus = AccountStatus.SIGNED_OUT private val accountListeners = ArrayList() @@ -72,6 +79,11 @@ class Accounts constructor(val context: Context) { Log.d(LOGTAG, "Account syncing has finished") isSyncing = false + + services.accountManager.accountProfile()?.email?.let { + SettingsStore.getInstance(context).setFxALastSync(it, getLastSynced(context)) + } + syncListeners.toMutableList().forEach { Handler(Looper.getMainLooper()).post { it.onIdle() @@ -108,11 +120,13 @@ class Accounts constructor(val context: Context) { override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { Log.d(LOGTAG, "The user has been successfully logged in") + if (authType !== AuthType.Existing) { + GleanMetricsService.FxA.signInResult(true) + } + accountStatus = AccountStatus.SIGNED_IN // Enable syncing after signing in - syncStorage.setStatus(SyncEngine.Bookmarks, SettingsStore.getInstance(context).isBookmarksSyncEnabled) - syncStorage.setStatus(SyncEngine.History, SettingsStore.getInstance(context).isHistorySyncEnabled) services.accountManager.syncNowAsync(SyncReason.EngineChange, true) // Update device list @@ -133,6 +147,8 @@ class Accounts constructor(val context: Context) { override fun onAuthenticationProblems() { Log.d(LOGTAG, "There was a problem authenticating the user") + GleanMetricsService.FxA.signInResult(false) + accountStatus = AccountStatus.NEEDS_RECONNECT accountListeners.toMutableList().forEach { Handler(Looper.getMainLooper()).post { @@ -150,6 +166,8 @@ class Accounts constructor(val context: Context) { it.onLoggedOut() } } + + loadDefaultProfilePicture() } override fun onProfileUpdated(profile: Profile) { @@ -160,6 +178,8 @@ class Accounts constructor(val context: Context) { it.onProfileUpdated(profile) } } + + loadProfilePicture(profile) } } @@ -181,6 +201,43 @@ class Accounts constructor(val context: Context) { } } + private fun loadProfilePicture(profile: Profile) { + CoroutineScope(Dispatchers.IO).launch { + try { + val url = URL(profile.avatar!!.url) + BitmapFactory.decodeStream(url.openStream())?.let { + val bitmap = ViewUtils.getRoundedCroppedBitmap(it) + profilePicture = BitmapDrawable(context.resources, bitmap) + BitmapCache.getInstance(context).addBitmap(PROFILE_PICTURE_TAG, bitmap) + + } ?: throw IllegalArgumentException() + + } catch (e: Exception) { + loadDefaultProfilePicture() + + } finally { + accountListeners.toMutableList().forEach { + Handler(Looper.getMainLooper()).post { + it.onProfileUpdated(profile) + } + } + } + } + } + + private fun loadDefaultProfilePicture(): BitmapDrawable? { + BitmapFactory.decodeResource(context.resources, R.drawable.ic_icon_settings_account)?.let { + try { + BitmapCache.getInstance(context).addBitmap(PROFILE_PICTURE_TAG, it) + } catch (e: NullPointerException) { + Log.w(LOGTAG, "Bitmap is a null pointer.") + return null + } + profilePicture = BitmapDrawable(context.resources, ViewUtils.getRoundedCroppedBitmap(it)) + } + + return profilePicture + } fun addAccountListener(aListener: AccountObserver) { if (!accountListeners.contains(aListener)) { @@ -225,6 +282,7 @@ class Accounts constructor(val context: Context) { } fun authUrlAsync(): CompletableFuture? { + GleanMetricsService.FxA.signIn() return CoroutineScope(Dispatchers.Main).future { services.accountManager.beginAuthenticationAsync().await() } @@ -256,10 +314,13 @@ class Accounts constructor(val context: Context) { } fun setSyncStatus(engine: SyncEngine, value: Boolean) { - when(engine) { - SyncEngine.Bookmarks -> SettingsStore.getInstance(context).isBookmarksSyncEnabled = value - SyncEngine.History -> SettingsStore.getInstance(context).isHistorySyncEnabled = value + SyncEngine.Bookmarks -> { + GleanMetricsService.FxA.bookmarksSyncStatus(value) + } + SyncEngine.History -> { + GleanMetricsService.FxA.historySyncStatus(value) + } } syncStorage.setStatus(engine, value) @@ -270,59 +331,14 @@ class Accounts constructor(val context: Context) { } fun logoutAsync(): CompletableFuture? { + GleanMetricsService.FxA.signOut() + otherDevices = emptyList() return CoroutineScope(Dispatchers.Main).future { services.accountManager.logoutAsync().await() } } - fun getAuthenticationUrlAsync(): CompletableFuture { - val future: CompletableFuture = CompletableFuture() - - // If we're already logged-in, and not in a "need to reconnect" state, logout. - if (services.accountManager.authenticatedAccount() != null && !services.accountManager.accountNeedsReauth()) { - services.accountManager.logoutAsync() - future.complete(null) - } - - // Otherwise, obtain an authentication URL and load it in the gecko session. - // Recovering from "need to reconnect" state is treated the same as just logging in. - val futureUrl = authUrlAsync() - if (futureUrl == null) { - Logger(LOGTAG).debug("Got a 'null' futureUrl") - services.accountManager.logoutAsync() - future.complete(null) - } - - Executors.newSingleThreadExecutor().submit { - try { - val url = futureUrl!!.get() - if (url == null) { - Logger(LOGTAG).debug("Got a 'null' url after resolving futureUrl") - services.accountManager.logoutAsync() - future.complete(null) - } - Logger(LOGTAG).debug("Got an auth url: " + url!!) - - // Actually process the url on the main thread. - Handler(Looper.getMainLooper()).post { - Logger(LOGTAG).debug("We got an authentication url, we can continue...") - future.complete(url) - } - - } catch (e: ExecutionException) { - Logger(LOGTAG).debug("Error obtaining auth url", e) - future.complete(null) - - } catch (e: InterruptedException) { - Logger(LOGTAG).debug("Error obtaining auth url", e) - future.complete(null) - } - } - - return future - } - fun isEngineEnabled(engine: SyncEngine): Boolean { return syncStorage.getStatus()[engine]?: false } @@ -332,7 +348,10 @@ class Accounts constructor(val context: Context) { } fun lastSync(): Long { - return getLastSynced(context) + services.accountManager.accountProfile()?.email?.let { + return SettingsStore.getInstance(context).getFxALastSync(it) + } + return 0 } fun devicesByCapability(capabilities: List): List { @@ -348,10 +367,10 @@ class Accounts constructor(val context: Context) { targetDevices.contains(it) } - targets?.forEach { + targets?.forEach { it -> constellation.sendEventToDeviceAsync( it.id, DeviceEventOutgoing.SendTab(title, url) - ).await() + ).await().also { if (it) GleanMetricsService.FxA.sentTab() } } } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/BookmarksStore.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/BookmarksStore.kt index 4fc9eaed2..cd24b9c7c 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/BookmarksStore.kt +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/BookmarksStore.kt @@ -59,7 +59,7 @@ class BookmarksStore constructor(val context: Context) { } private val listeners = ArrayList() - private val storage = (context.applicationContext as VRBrowserApplication).places.bookmarks + private var storage = (context.applicationContext as VRBrowserApplication).places.bookmarks private val titles = rootTitles(context) private val accountManager = (context.applicationContext as VRBrowserApplication).services.accountManager @@ -100,6 +100,11 @@ class BookmarksStore constructor(val context: Context) { listeners.clear() } + internal fun updateStorage() { + storage = (context.applicationContext as VRBrowserApplication).places.bookmarks + notifyListeners() + } + fun getBookmarks(guid: String): CompletableFuture?> = GlobalScope.future { when (guid) { BookmarkRoot.Mobile.id -> { @@ -157,9 +162,7 @@ class BookmarksStore constructor(val context: Context) { fun getTree(guid: String, recursive: Boolean): CompletableFuture?> = GlobalScope.future { storage.getTree(guid, recursive)?.children - ?.map { - it.copy(title = titles[it.guid]) - } + ?.map { it.copy(title = titles[it.guid]) } } fun searchBookmarks(query: String, limit: Int): CompletableFuture> = GlobalScope.future { 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 c2f3584a4..f8b7dfe17 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/HistoryStore.kt +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/HistoryStore.kt @@ -23,7 +23,7 @@ class HistoryStore constructor(val context: Context) { private val LOGTAG = SystemUtils.createLogtag(HistoryStore::class.java) private var listeners = ArrayList() - private val storage = (context.applicationContext as VRBrowserApplication).places.history + private var storage = (context.applicationContext as VRBrowserApplication).places.history // Bookmarks might have changed during sync, so notify our listeners. private val syncStatusObserver = object : SyncStatusObserver { @@ -61,6 +61,11 @@ class HistoryStore constructor(val context: Context) { listeners.clear() } + internal fun updateStorage() { + storage = (context.applicationContext as VRBrowserApplication).places.history + notifyListeners() + } + fun getHistory(): CompletableFuture?> = GlobalScope.future { storage.getVisited() } @@ -68,14 +73,22 @@ class HistoryStore constructor(val context: Context) { fun getDetailedHistory(): CompletableFuture?> = GlobalScope.future { storage.getDetailedVisits(0, excludeTypes = listOf( VisitType.NOT_A_VISIT, + VisitType.DOWNLOAD, VisitType.REDIRECT_TEMPORARY, + VisitType.RELOAD, + VisitType.EMBED, + VisitType.FRAMED_LINK, VisitType.REDIRECT_PERMANENT)) } fun getVisitsPaginated(offset: Long, count: Long): CompletableFuture?> = GlobalScope.future { storage.getVisitsPaginated(offset, count, excludeTypes = listOf( VisitType.NOT_A_VISIT, + VisitType.DOWNLOAD, VisitType.REDIRECT_TEMPORARY, + VisitType.RELOAD, + VisitType.EMBED, + VisitType.FRAMED_LINK, VisitType.REDIRECT_PERMANENT)) } 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 d3d8b0b27..572aceb7a 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/PermissionDelegate.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/PermissionDelegate.java @@ -68,7 +68,6 @@ public void handlePermission(final String aUri, final PermissionWidget.Permissio mWidgetManager.addWidget(mPermissionWidget); } - mPermissionWidget.getPlacement().parentHandle = mParentWidgetHandle; mPermissionWidget.showPrompt(aUri, aType, aCallback); } @@ -115,6 +114,20 @@ public void onAndroidPermissionsRequest(GeckoSession aSession, String[] permissi @Override public void onContentPermissionRequest(GeckoSession aSession, String aUri, int aType, Callback callback) { Log.d(LOGTAG, "onContentPermissionRequest: " + aUri + " " + aType); + if (aType == PERMISSION_XR) { + callback.grant(); + return; + } + + if (aType == PERMISSION_AUTOPLAY_AUDIBLE || aType == PERMISSION_AUTOPLAY_INAUDIBLE) { + if (SettingsStore.getInstance(mContext).isAutoplayEnabled()) { + callback.grant(); + } else { + callback.reject(); + } + return; + } + PermissionWidget.PermissionType type; if (aType == PERMISSION_DESKTOP_NOTIFICATION) { type = PermissionWidget.PermissionType.Notification; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/Places.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/Places.kt index fe6a6410c..7b00a9779 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/Places.kt +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/Places.kt @@ -8,11 +8,42 @@ package org.mozilla.vrbrowser.browser import android.content.Context import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesHistoryStorage +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.vrbrowser.browser.engine.SessionStore +import org.mozilla.vrbrowser.utils.SystemUtils /** * Entry point for interacting with places-backed storage layers. */ -class Places(context: Context) { - val bookmarks by lazy { PlacesBookmarksStorage(context) } - val history by lazy { PlacesHistoryStorage(context) } +class Places(var context: Context) { + + private val LOGTAG = SystemUtils.createLogtag(Places::class.java) + + var bookmarks = PlacesBookmarksStorage(context) + var history = PlacesHistoryStorage(context) + + fun clear() { + val files = context.filesDir.listFiles { dir, name -> + name.matches("places\\.sqlite.*".toRegex()) + } + for (file in files) { + if (!file.delete()) { + Logger(LOGTAG).debug("Can't remove " + file.absolutePath) + } + } + + bookmarks.cleanup() + // We create a new storage, otherwise we would need to restart the app so it's created in the Application onCreate + bookmarks = PlacesBookmarksStorage(context) + // Update the storage in the proxy class + SessionStore.get().bookmarkStore.updateStorage() + + // We are supposed to call this but it fails internally as apparently the PlacesStorage is common + // and it's already being cleaned after bookmarks.cleanup() + // history.cleanup() + // We create a new storage, otherwise we would need to restart the app so it's created in the Application onCreate + history = PlacesHistoryStorage(context) + // Update the storage in the proxy class + SessionStore.get().historyStore.updateStorage() + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/PromptDelegate.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/PromptDelegate.java index b42c7a355..d42062be5 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/PromptDelegate.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/PromptDelegate.java @@ -132,7 +132,7 @@ public GeckoResult onAlertPrompt(@NonNull GeckoSession geckoSess mPrompt = new AlertPromptWidget(mContext); mPrompt.getPlacement().parentHandle = mAttachedWindow.getHandle(); mPrompt.getPlacement().parentAnchorY = 0.0f; - mPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.base_app_dialog_y_distance); + mPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.js_prompt_y_distance); mPrompt.setTitle(alertPrompt.title); mPrompt.setMessage(alertPrompt.message); mPrompt.setPromptDelegate(() -> result.complete(alertPrompt.dismiss())); @@ -149,7 +149,7 @@ public GeckoResult onButtonPrompt(@NonNull GeckoSession geckoSes mPrompt = new ConfirmPromptWidget(mContext); mPrompt.getPlacement().parentHandle = mAttachedWindow.getHandle(); mPrompt.getPlacement().parentAnchorY = 0.0f; - mPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.base_app_dialog_y_distance); + mPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.js_prompt_y_distance); mPrompt.setTitle(buttonPrompt.title); mPrompt.setMessage(buttonPrompt.message); ((ConfirmPromptWidget)mPrompt).setButtons(new String[] { @@ -180,7 +180,7 @@ public GeckoResult onTextPrompt(@NonNull GeckoSession geckoSessi mPrompt = new TextPromptWidget(mContext); mPrompt.getPlacement().parentHandle = mAttachedWindow.getHandle(); mPrompt.getPlacement().parentAnchorY = 0.0f; - mPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.base_app_dialog_y_distance); + mPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.js_prompt_y_distance); mPrompt.setTitle(textPrompt.title); mPrompt.setMessage(textPrompt.message); ((TextPromptWidget)mPrompt).setDefaultText(textPrompt.defaultValue); @@ -208,7 +208,7 @@ public GeckoResult onAuthPrompt(@NonNull GeckoSession geckoSessi mPrompt = new AuthPromptWidget(mContext); mPrompt.getPlacement().parentHandle = mAttachedWindow.getHandle(); mPrompt.getPlacement().parentAnchorY = 0.0f; - mPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.base_app_dialog_y_distance); + mPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.js_prompt_y_distance); mPrompt.setTitle(authPrompt.title); mPrompt.setMessage(authPrompt.message); ((AuthPromptWidget)mPrompt).setAuthOptions(authPrompt.authOptions); @@ -241,7 +241,7 @@ public GeckoResult onChoicePrompt(@NonNull GeckoSession geckoSes mPrompt = new ChoicePromptWidget(mContext); mPrompt.getPlacement().parentHandle = mAttachedWindow.getHandle(); mPrompt.getPlacement().parentAnchorY = 0.0f; - mPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.base_app_dialog_y_distance); + mPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.js_prompt_y_distance); mPrompt.setTitle(choicePrompt.title); mPrompt.setMessage(choicePrompt.message); ((ChoicePromptWidget)mPrompt).setChoices(choicePrompt.choices); @@ -352,10 +352,6 @@ private void showPopUp(int sessionId, @NonNull Pair site = mAllowedPopUpSites.stream().filter((item) -> item.url.equals(uri)).findFirst(); if (!site.isPresent()) { mPopUpPrompt = new PopUpBlockDialogWidget(mContext); - mPopUpPrompt.getPlacement().parentHandle = mAttachedWindow.getHandle(); - mPopUpPrompt.getPlacement().parentAnchorY = 0.0f; - mPopUpPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.base_app_dialog_y_distance); - mPopUpPrompt.setTitle(uri); mPopUpPrompt.setButtonsDelegate(index -> { boolean allowed = index != PopUpBlockDialogWidget.NEGATIVE; boolean askAgain = mPopUpPrompt.askAgain(); @@ -384,7 +380,14 @@ private void showPopUp(int sessionId, @NonNull Pair mExecutors.mainThread().execute(() -> { + if (mPopupDelegate != null) { + mPopupDelegate.onPopUpAvailable(); + } + })); mPopUpPrompt.show(UIWidget.REQUEST_FOCUS); } else { @@ -407,7 +410,7 @@ public GeckoResult onSlowScript(@NonNull GeckoSession aSessi mSlowScriptPrompt = new ConfirmPromptWidget(mContext); mSlowScriptPrompt.getPlacement().parentHandle = mAttachedWindow.getHandle(); mSlowScriptPrompt.getPlacement().parentAnchorY = 0.0f; - mSlowScriptPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.base_app_dialog_y_distance); + mSlowScriptPrompt.getPlacement().translationY = WidgetPlacement.unitFromMeters(mContext, R.dimen.js_prompt_y_distance); mSlowScriptPrompt.setTitle(mContext.getResources().getString(R.string.slow_script_dialog_title)); mSlowScriptPrompt.setMessage(mContext.getResources().getString(R.string.slow_script_dialog_description, aScriptFileName)); mSlowScriptPrompt.setButtons(new String[]{ diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt index cf92d931a..c8c9ec72e 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/Services.kt @@ -14,8 +14,9 @@ import androidx.work.WorkManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import mozilla.appservices.Megazord +import mozilla.appservices.rustlog.LogAdapterCannotEnable import mozilla.components.concept.sync.* -import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient import mozilla.components.service.fxa.* import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider @@ -28,8 +29,15 @@ import org.mozilla.geckoview.AllowOrDeny import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoSession import org.mozilla.vrbrowser.R +import org.mozilla.vrbrowser.browser.engine.EngineProvider +import org.mozilla.vrbrowser.utils.SystemUtils +import org.mozilla.vrbrowser.telemetry.GleanMetricsService + + +class Services(val context: Context, places: Places): GeckoSession.NavigationDelegate { + + private val LOGTAG = SystemUtils.createLogtag(Services::class.java) -class Services(context: Context, places: Places): GeckoSession.NavigationDelegate { companion object { const val CLIENT_ID = "7ad9917f6c55fb77" const val REDIRECT_URL = "https://accounts.firefox.com/oauth/success/$CLIENT_ID" @@ -42,8 +50,13 @@ class Services(context: Context, places: Places): GeckoSession.NavigationDelegat // This makes bookmarks storage accessible to background sync workers. init { - RustLog.enable() - RustHttpConfig.setClient(lazy { HttpURLConnectionClient() }) + Megazord.init() + try { + RustLog.enable() + } catch (e: LogAdapterCannotEnable) { + android.util.Log.w(LOGTAG, "RustLog has been enabled.") + } + RustHttpConfig.setClient(lazy { EngineProvider.createClient(context) }) // Make sure we get logs out of our android-components. Log.addSink(AndroidLogSink()) @@ -76,7 +89,10 @@ class Services(context: Context, places: Places): GeckoSession.NavigationDelegat Logger(logTag).info("Received ${events.size} device event(s)") val filteredEvents = events.filterIsInstance(DeviceEvent.TabReceived::class.java) if (filteredEvents.isNotEmpty()) { - val tabs = filteredEvents.map { event -> event.entries }.flatten() + filteredEvents.map { event -> event.from?.deviceType?.let { GleanMetricsService.FxA.receivedTab(it) } } + val tabs = filteredEvents.map { + event -> event.entries + }.flatten() tabReceivedDelegate?.onTabsReceived(tabs) } } 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 f9b1d85b8..8250856fa 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java @@ -5,21 +5,23 @@ import android.graphics.Color; import android.os.StrictMode; import android.preference.PreferenceManager; +import android.util.Log; + +import androidx.annotation.NonNull; import org.json.JSONArray; +import org.json.JSONObject; import org.mozilla.geckoview.GeckoSessionSettings; import org.mozilla.telemetry.TelemetryHolder; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.utils.DeviceType; -import org.mozilla.vrbrowser.utils.LocaleUtils; import org.mozilla.vrbrowser.utils.StringUtils; import org.mozilla.vrbrowser.utils.SystemUtils; -import androidx.annotation.NonNull; - import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -47,7 +49,8 @@ SettingsStore getInstance(final @NonNull Context aContext) { public final static boolean REMOTE_DEBUGGING_DEFAULT = false; public final static boolean CONSOLE_LOGS_DEFAULT = false; public final static boolean ENV_OVERRIDE_DEFAULT = false; - public final static boolean MULTIPROCESS_DEFAULT = true; + public final static boolean MULTIPROCESS_DEFAULT = false; + public final static boolean UI_HARDWARE_ACCELERATION_DEFAULT = true; public final static boolean PERFORMANCE_MONITOR_DEFAULT = true; public final static boolean DRM_PLAYBACK_DEFAULT = false; public final static boolean TRACKING_DEFAULT = true; @@ -79,6 +82,8 @@ SettingsStore getInstance(final @NonNull Context aContext) { public final static boolean BOOKMARKS_SYNC_DEFAULT = true; public final static boolean HISTORY_SYNC_DEFAULT = true; public final static boolean WHATS_NEW_DISPLAYED = false; + public final static long FXA_LAST_SYNC_NEVER = 0; + public final static boolean RESTORE_TABS_ENABLED = true; // Enable telemetry by default (opt-out). public final static boolean CRASH_REPORTING_DEFAULT = false; @@ -234,6 +239,17 @@ public void setMultiprocessEnabled(boolean isEnabled) { editor.commit(); } + public boolean isUIHardwareAccelerationEnabled() { + return mPrefs.getBoolean( + mContext.getString(R.string.settings_key_ui_hardware_acceleration), UI_HARDWARE_ACCELERATION_DEFAULT); + } + + public void setUIHardwareAccelerationEnabled(boolean isEnabled) { + SharedPreferences.Editor editor = mPrefs.edit(); + editor.putBoolean(mContext.getString(R.string.settings_key_ui_hardware_acceleration), isEnabled); + editor.commit(); + } + public boolean isPerformanceMonitorEnabled() { // Disabling Performance Monitor until it can properly handle multi-window return false; // mPrefs.getBoolean(mContext.getString(R.string.settings_key_performance_monitor), PERFORMANCE_MONITOR_DEFAULT); @@ -402,9 +418,6 @@ public void setAudioEnabled(boolean isEnabled) { public String getVoiceSearchLocale() { String language = mPrefs.getString( mContext.getString(R.string.settings_key_voice_search_language), null); - if (language == null) { - return LocaleUtils.getDefaultVoiceSearchLocale(mContext); - } return language; } @@ -417,9 +430,6 @@ public void setVoiceSearchLocale(String language) { public String getDisplayLocale() { String language = mPrefs.getString( mContext.getString(R.string.settings_key_display_language), null); - if (language == null) { - return LocaleUtils.getDefaultDisplayLocale(mContext); - } return language; } @@ -565,7 +575,7 @@ public void setSpeechDataCollectionReviewed(boolean isEnabled) { editor.commit(); } - public boolean isDebugLogginEnabled() { + public boolean isDebugLoggingEnabled() { return mPrefs.getBoolean(mContext.getString(R.string.settings_key_debug_logging), DEBUG_LOGGING_DEFAULT); } @@ -604,34 +614,65 @@ public void setPopUpsBlockingEnabled(boolean isEnabled) { editor.putBoolean(mContext.getString(R.string.settings_key_pop_up_blocking), isEnabled); editor.commit(); } - public void setBookmarksSyncEnabled(boolean isEnabled) { + + public void setWhatsNewDisplayed(boolean isEnabled) { SharedPreferences.Editor editor = mPrefs.edit(); - editor.putBoolean(mContext.getString(R.string.settings_key_bookmarks_sync), isEnabled); + editor.putBoolean(mContext.getString(R.string.settings_key_whats_new_displayed), isEnabled); editor.commit(); } - public boolean isBookmarksSyncEnabled() { - return mPrefs.getBoolean(mContext.getString(R.string.settings_key_bookmarks_sync), BOOKMARKS_SYNC_DEFAULT); + public boolean isWhatsNewDisplayed() { + return mPrefs.getBoolean(mContext.getString(R.string.settings_key_whats_new_displayed), WHATS_NEW_DISPLAYED); } - public void setHistorySyncEnabled(boolean isEnabled) { - SharedPreferences.Editor editor = mPrefs.edit(); - editor.putBoolean(mContext.getString(R.string.settings_key_history_sync), isEnabled); - editor.commit(); + public void setFxALastSync(@NonNull String email, long timestamp) { + String json = mPrefs.getString( + mContext.getString(R.string.settings_key_fxa_last_sync), + new JSONObject().toString()); + + try { + JSONObject jsonObject = new JSONObject(json); + jsonObject.put(email, timestamp); + + SharedPreferences.Editor editor = mPrefs.edit(); + editor.putString(mContext.getString(R.string.settings_key_fxa_last_sync), jsonObject.toString()); + editor.commit(); + + } catch (Exception e) { + Log.d(LOGTAG, e.getMessage()); + } } - public boolean isHistorySyncEnabled() { - return mPrefs.getBoolean(mContext.getString(R.string.settings_key_history_sync), HISTORY_SYNC_DEFAULT); + public long getFxALastSync(@NonNull String email) { + String json = mPrefs.getString( + mContext.getString(R.string.settings_key_fxa_last_sync), + null); + + try { + JSONObject jsonObject = new JSONObject(json); + Iterator iterator = jsonObject.keys(); + while (iterator.hasNext()) { + String key = iterator.next(); + if (key.equals(email)) { + return jsonObject.getLong(key); + } + } + + return FXA_LAST_SYNC_NEVER; + + } catch (Exception e) { + return FXA_LAST_SYNC_NEVER; + } } - public void setWhatsNewDisplayed(boolean isEnabled) { + public void setRestoreTabsEnabled(boolean isEnabled) { SharedPreferences.Editor editor = mPrefs.edit(); - editor.putBoolean(mContext.getString(R.string.settings_key_whats_new_displayed), isEnabled); + editor.putBoolean(mContext.getString(R.string.settings_key_restore_tabs), isEnabled); editor.commit(); } - public boolean isWhatsNewDisplayed() { - return mPrefs.getBoolean(mContext.getString(R.string.settings_key_whats_new_displayed), WHATS_NEW_DISPLAYED); + public boolean isRestoreTabsEnabled() { + return mPrefs.getBoolean(mContext.getString(R.string.settings_key_restore_tabs), RESTORE_TABS_ENABLED); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/EngineProvider.kt b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/EngineProvider.kt new file mode 100644 index 000000000..1c73717be --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/EngineProvider.kt @@ -0,0 +1,67 @@ +/* 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.browser.engine + +import android.content.Context +import mozilla.components.concept.fetch.Client +import org.mozilla.geckoview.ContentBlocking +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoRuntimeSettings +import org.mozilla.geckoview.WebExtension +import org.mozilla.vrbrowser.BuildConfig +import org.mozilla.vrbrowser.browser.SettingsStore +import org.mozilla.vrbrowser.crashreporting.CrashReporterService + +object EngineProvider { + + private val WEB_EXTENSIONS = arrayOf("webcompat_vimeo", "webcompat_youtube") + + private var runtime: GeckoRuntime? = null + + @Synchronized + fun getOrCreateRuntime(context: Context): GeckoRuntime { + if (runtime == null) { + val builder = GeckoRuntimeSettings.Builder() + + builder.crashHandler(CrashReporterService::class.java) + builder.contentBlocking(ContentBlocking.Settings.Builder() + .antiTracking(ContentBlocking.AntiTracking.AD or ContentBlocking.AntiTracking.SOCIAL or ContentBlocking.AntiTracking.ANALYTIC) + .build()) + builder.consoleOutput(SettingsStore.getInstance(context).isConsoleLogsEnabled) + builder.displayDensityOverride(SettingsStore.getInstance(context).displayDensity) + builder.remoteDebuggingEnabled(SettingsStore.getInstance(context).isRemoteDebuggingEnabled) + builder.displayDpiOverride(SettingsStore.getInstance(context).displayDpi) + builder.screenSizeOverride(SettingsStore.getInstance(context).maxWindowWidth, + SettingsStore.getInstance(context).maxWindowHeight) + + if (SettingsStore.getInstance(context).transparentBorderWidth > 0) { + builder.useMaxScreenDepth(true) + } + + if (BuildConfig.DEBUG) { + builder.arguments(arrayOf("-purgecaches")) + builder.debugLogging(true) + builder.aboutConfigEnabled(true) + } else { + builder.debugLogging(SettingsStore.getInstance(context).isDebugLoggingEnabled) + } + + runtime = GeckoRuntime.create(context, builder.build()) + for (extension in WEB_EXTENSIONS) { + val path = "resource://android/assets/web_extensions/$extension/" + runtime!!.registerWebExtension(WebExtension(path, runtime!!.webExtensionController)) + } + + + } + + return runtime!! + } + + fun createClient(context: Context): Client { + val runtime = getOrCreateRuntime(context) + return GeckoViewFetchClient(context, runtime) + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java index 32ea5cc73..d8ca3003d 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java @@ -24,7 +24,6 @@ import org.mozilla.geckoview.AllowOrDeny; import org.mozilla.geckoview.ContentBlocking; import org.mozilla.geckoview.GeckoDisplay; -import org.mozilla.geckoview.GeckoResponse; import org.mozilla.geckoview.GeckoResult; import org.mozilla.geckoview.GeckoRuntime; import org.mozilla.geckoview.GeckoSession; @@ -39,6 +38,7 @@ import org.mozilla.vrbrowser.browser.UserAgentOverride; import org.mozilla.vrbrowser.browser.VideoAvailabilityListener; import org.mozilla.vrbrowser.geolocation.GeolocationData; +import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.utils.BitmapCache; import org.mozilla.vrbrowser.utils.InternalPages; @@ -46,7 +46,8 @@ import java.net.URI; import java.net.URISyntaxException; -import java.util.LinkedList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -62,19 +63,19 @@ public class Session implements ContentBlocking.Delegate, GeckoSession.Navigatio private static final String LOGTAG = SystemUtils.createLogtag(Session.class); private static UserAgentOverride sUserAgentOverride; + private static final long KEEP_ALIVE_DURATION_MS = 1000; // 1 second. - - private transient LinkedList mNavigationListeners; - private transient LinkedList mProgressListeners; - private transient LinkedList mContentListeners; - private transient LinkedList mSessionChangeListeners; - private transient LinkedList mTextInputListeners; - private transient LinkedList mVideoAvailabilityListeners; - private transient LinkedList mBitmapChangedListeners; - private transient LinkedList mSelectionActionListeners; + private transient CopyOnWriteArrayList mNavigationListeners; + private transient CopyOnWriteArrayList mProgressListeners; + private transient CopyOnWriteArrayList mContentListeners; + private transient CopyOnWriteArrayList mSessionChangeListeners; + private transient CopyOnWriteArrayList mTextInputListeners; + private transient CopyOnWriteArrayList mVideoAvailabilityListeners; + private transient CopyOnWriteArrayList mBitmapChangedListeners; + private transient CopyOnWriteArrayList mSelectionActionListeners; private SessionState mState; - private LinkedList mQueuedCalls = new LinkedList<>(); + private transient CopyOnWriteArrayList mQueuedCalls = new CopyOnWriteArrayList<>(); private transient GeckoSession.PermissionDelegate mPermissionDelegate; private transient GeckoSession.PromptDelegate mPromptDelegate; private transient GeckoSession.HistoryDelegate mHistoryDelegate; @@ -82,7 +83,8 @@ public class Session implements ContentBlocking.Delegate, GeckoSession.Navigatio private transient SharedPreferences mPrefs; private transient GeckoRuntime mRuntime; private transient byte[] mPrivatePage; - + private transient boolean mFirstContentfulPaint; + private transient long mKeepAlive; public interface BitmapChangedListener { void onBitmapChanged(Session aSession, Bitmap aBitmap); @@ -99,6 +101,7 @@ protected Session(Context aContext, GeckoRuntime aRuntime, mRuntime = aRuntime; initialize(); mState = createSession(aSettings, aOpenMode); + mState.setActive(true); } protected Session(Context aContext, GeckoRuntime aRuntime, @NonNull SessionState aRestoreState) { @@ -109,14 +112,14 @@ protected Session(Context aContext, GeckoRuntime aRuntime, @NonNull SessionState } private void initialize() { - mNavigationListeners = new LinkedList<>(); - mProgressListeners = new LinkedList<>(); - mContentListeners = new LinkedList<>(); - mSessionChangeListeners = new LinkedList<>(); - mTextInputListeners = new LinkedList<>(); - mVideoAvailabilityListeners = new LinkedList<>(); - mSelectionActionListeners = new LinkedList<>(); - mBitmapChangedListeners = new LinkedList<>(); + mNavigationListeners = new CopyOnWriteArrayList<>(); + mProgressListeners = new CopyOnWriteArrayList<>(); + mContentListeners = new CopyOnWriteArrayList<>(); + mSessionChangeListeners = new CopyOnWriteArrayList<>(); + mTextInputListeners = new CopyOnWriteArrayList<>(); + mVideoAvailabilityListeners = new CopyOnWriteArrayList<>(); + mSelectionActionListeners = new CopyOnWriteArrayList<>(); + mBitmapChangedListeners = new CopyOnWriteArrayList<>(); if (mPrefs != null) { mPrefs.registerOnSharedPreferenceChangeListener(this); @@ -185,7 +188,7 @@ private void dumpAllState() { private void dumpState(GeckoSession.NavigationDelegate aListener) { if (mState.mSession != null) { - aListener.onCanGoBack(mState.mSession, mState.mCanGoBack); + aListener.onCanGoBack(mState.mSession, canGoBack()); aListener.onCanGoForward(mState.mSession, mState.mCanGoForward); aListener.onLocationChange(mState.mSession, mState.mUri); } @@ -325,18 +328,41 @@ private void cleanSessionListeners(GeckoSession aSession) { } public void suspend() { - if (mState.mIsActive) { + if (mState.isActive()) { Log.e(LOGTAG, "Active Sessions can not be suspended"); return; } if (mState.mSession == null) { return; } + if (mKeepAlive > System.currentTimeMillis()) { + Log.e(LOGTAG, "Unable to suspend activity with active keep alive time."); + return; + } + Log.d(LOGTAG, "Suspending Session: " + mState.mId); closeSession(mState); mState.mSession = null; } + private boolean shouldLoadDefaultPage(@NonNull SessionState aState) { + if (aState.mUri != null && aState.mUri.length() != 0 && !aState.mUri.equals(mContext.getString(R.string.about_blank))) { + return false; + } + if (aState.mSessionState != null && aState.mSessionState.size() != 0) { + return false; + } + return true; + } + + private void loadDefaultPage() { + if (mState.mSettings.isPrivateBrowsingEnabled()) { + loadPrivateBrowsingPage(); + } else { + loadHomePage(); + } + } + private void restore() { SessionSettings settings = mState.mSettings; if (settings == null) { @@ -345,29 +371,32 @@ private void restore() { .build(); } - String restoreUri = mState.mUri; - mState.mSession = createGeckoSession(settings); if (!mState.mSession.isOpen()) { mState.mSession.open(mRuntime); } - if (mState.mSessionState != null) { - mState.mSession.restoreState(mState.mSessionState); + // data:text URLs can not be restored. + if (mState.mSessionState != null && ((mState.mUri == null) || mState.mUri.startsWith("data:text"))) { + mState.mSessionState = null; + mState.mUri = null; } - if ((mState.mSessionState == null) && (restoreUri != null)) { - mState.mSession.loadUri(restoreUri); - } else if (mState.mSettings.isPrivateBrowsingEnabled() && mState.mUri == null) { - loadPrivateBrowsingPage(); - } else if(mState.mSessionState == null || mState.mUri.equals(mContext.getResources().getString(R.string.about_blank)) || - (mState.mSessionState != null && mState.mSessionState.size() == 0)) { - loadHomePage(); - } else if (mState.mUri != null && mState.mUri.contains(".youtube.com")) { - mState.mSession.loadUri(mState.mUri, GeckoSession.LOAD_FLAGS_REPLACE_HISTORY); + if (shouldLoadDefaultPage(mState)) { + loadDefaultPage(); + } else if (mState.mSessionState != null) { + mState.mSession.restoreState(mState.mSessionState); + if (mState.mUri != null && mState.mUri.contains(".youtube.com")) { + mState.mSession.loadUri(mState.mUri, GeckoSession.LOAD_FLAGS_REPLACE_HISTORY); + } + } else if (mState.mUri != null) { + mState.mSession.loadUri(mState.mUri); + } else { + loadDefaultPage(); } dumpAllState(); + mState.setActive(true); } @@ -406,19 +435,19 @@ private GeckoSession createGeckoSession(@NonNull SessionSettings aSettings) { return session; } - private void recreateSession() { + public void recreateSession() { SessionState previous = mState; + mState = mState.recreate(); + restore(); - mState = createSession(previous.mSettings, SESSION_OPEN); - if (previous.mSessionState != null) { - mState.mSession.restoreState(previous.mSessionState); - } + GeckoSession previousGeckoSession = null; if (previous.mSession != null) { + previousGeckoSession = previous.mSession; closeSession(previous); } for (SessionChangeListener listener : mSessionChangeListeners) { - listener.onCurrentSessionChange(previous.mSession, mState.mSession); + listener.onCurrentSessionChange(previousGeckoSession, mState.mSession); } } @@ -435,58 +464,80 @@ private void closeSession(@NonNull SessionState aState) { aState.mDisplay = null; } aState.mSession.close(); - aState.mIsActive = false; + aState.setActive(false); + mFirstContentfulPaint = false; } public void captureBitmap() { - if (mState.mDisplay == null) { + if (mState.mDisplay == null || !mFirstContentfulPaint) { return; } - mState.mDisplay.capturePixels().then(bitmap -> { - if (bitmap != null) { - BitmapCache.getInstance(mContext).scaleBitmap(bitmap, 500, 280).thenAccept(scaledBitmap -> { - BitmapCache.getInstance(mContext).addBitmap(getId(), scaledBitmap); + try { + mState.mDisplay.screenshot().aspectPreservingSize(500).capture().then(bitmap -> { + if (bitmap != null) { + BitmapCache.getInstance(mContext).addBitmap(getId(), bitmap); for (BitmapChangedListener listener: mBitmapChangedListeners) { - listener.onBitmapChanged(Session.this, scaledBitmap); + listener.onBitmapChanged(Session.this, bitmap); } + } + return null; + }).exceptionally(throwable -> { + Log.e(LOGTAG, "Error capturing session bitmap"); + throwable.printStackTrace(); + return null; + }); + } catch (Exception ex) { + Log.e(LOGTAG, "Error capturing session bitmap"); + ex.printStackTrace(); + } - }).exceptionally(throwable -> { - Log.d(LOGTAG, "Error scaling the bitmap: " + throwable.getLocalizedMessage()); - throwable.printStackTrace(); - return null; - }); - } - return null; - }); } - public void captureBackgroundBitmap(int displayWidth, int displayHeight) { + public CompletableFuture captureBackgroundBitmap(int displayWidth, int displayHeight) { + if (mState.mSession == null || !mFirstContentfulPaint) { + return CompletableFuture.completedFuture(null); + } Surface captureSurface = BitmapCache.getInstance(mContext).acquireCaptureSurface(displayWidth, displayHeight); if (captureSurface == null) { - return; + return CompletableFuture.completedFuture(null); } - GeckoSession session = mState.mSession; - GeckoDisplay display = session.acquireDisplay(); + + CompletableFuture result = new CompletableFuture<>(); + GeckoDisplay display = mState.mSession.acquireDisplay(); display.surfaceChanged(captureSurface, displayWidth, displayHeight); - display.capturePixels().then(bitmap -> { - if (bitmap != null) { - BitmapCache.getInstance(mContext).scaleBitmap(bitmap, 500, 280).thenAccept(scaledBitmap -> { - BitmapCache.getInstance(mContext).addBitmap(getId(), scaledBitmap); - for (BitmapChangedListener listener: mBitmapChangedListeners) { - listener.onBitmapChanged(Session.this, scaledBitmap); - } - }).exceptionally(throwable -> { - Log.d(LOGTAG, "Error scaling the bitmap: " + throwable.getLocalizedMessage()); - throwable.printStackTrace(); - return null; - }); - } + Runnable cleanResources = () -> { display.surfaceDestroyed(); - session.releaseDisplay(display); + mState.mSession.releaseDisplay(display); BitmapCache.getInstance(mContext).releaseCaptureSurface(); - return null; - }); + }; + + try { + display.screenshot().aspectPreservingSize(500).capture().then(bitmap -> { + if (bitmap != null) { + BitmapCache.getInstance(mContext).addBitmap(getId(), bitmap); + for (BitmapChangedListener listener : mBitmapChangedListeners) { + listener.onBitmapChanged(Session.this, bitmap); + } + } + cleanResources.run(); + result.complete(null); + return null; + }).exceptionally(throwable -> { + Log.e(LOGTAG, "Error capturing session background bitmap"); + throwable.printStackTrace(); + cleanResources.run(); + result.complete(null); + return null; + }); + } + catch (Exception ex) { + Log.e(LOGTAG, "Error capturing session background bitmap"); + ex.printStackTrace(); + cleanResources.run(); + result.complete(null); + } + return result; } public boolean hasCapturedBitmap() { @@ -545,6 +596,10 @@ public boolean isVideoAvailable() { return mState.mMediaElements != null && mState.mMediaElements.size() > 0; } + public boolean isFirstContentfulPaint() { + return mFirstContentfulPaint; + } + public Media getFullScreenVideo() { for (Media media: mState.mMediaElements) { if (media.isFullscreen()) { @@ -596,18 +651,21 @@ public void goForward() { public void setActive(boolean aActive) { // Flush the events queued while the session was inactive - if (mState.mSession != null && !mState.mIsActive && aActive) { + if (mState.mSession != null && !mState.isActive() && aActive) { flushQueuedEvents(); } if (mState.mSession != null) { - mState.mSession.setActive(aActive); - } else { + if (mState.isActive() != aActive) { + mState.mSession.setActive(aActive); + } + mState.setActive(aActive); + } else if (aActive) { restore(); + } else { + Log.e(LOGTAG, "ERROR: Setting null GeckoView to inactive!"); } - mState.mIsActive = aActive; - for (SessionChangeListener listener: mSessionChangeListeners) { listener.onActiveStateChange(this, aActive); } @@ -661,6 +719,7 @@ public void toggleServo() { mState = createSession(settings, SESSION_OPEN); closeSession(previous); + mState.setActive(true); loadUri(uri); } @@ -682,6 +741,7 @@ public String getId() { return mState.mId; } + public boolean isPrivateMode() { if (mState.mSession != null) { return mState.mSession.getSettings().getUsePrivateMode(); @@ -705,7 +765,7 @@ public int getUaMode() { } public boolean isActive() { - return mState.mIsActive; + return mState.isActive(); } private static final String M_PREFIX = "m."; @@ -769,25 +829,9 @@ protected void resetMultiprocess() { protected void setTrackingProtection(final boolean aEnabled) { if (mState.mSettings.isTrackingProtectionEnabled() != aEnabled) { mState.mSettings.setTrackingProtectionEnabled(aEnabled); - mState.mSession.getSettings().setUseTrackingProtection(aEnabled); - } - } - - public void clearCache(final long clearFlags) { - if (mRuntime != null) { - // Per GeckoView Docs: - // Note: Any open session may re-accumulate previously cleared data. - // To ensure that no persistent data is left behind, you need to close all sessions prior to clearing data. - // https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/StorageController.html#clearData-long- if (mState.mSession != null) { - mState.mSession.stop(); - mState.mSession.close(); + mState.mSession.getSettings().setUseTrackingProtection(aEnabled); } - - mRuntime.getStorageController().clearData(clearFlags).then(aVoid -> { - recreateSession(); - return null; - }); } } @@ -825,15 +869,15 @@ public void onLocationChange(@NonNull GeckoSession aSession, String aUri) { } @Override - public void onCanGoBack(@NonNull GeckoSession aSession, boolean aCanGoBack) { + public void onCanGoBack(@NonNull GeckoSession aSession, boolean aGeckoSessionCanGoBack) { if (mState.mSession != aSession) { return; } - Log.d(LOGTAG, "Session onCanGoBack: " + (aCanGoBack ? "true" : "false")); - mState.mCanGoBack = aCanGoBack; + Log.d(LOGTAG, "Session onCanGoBack: " + (aGeckoSessionCanGoBack ? "true" : "false")); + mState.mCanGoBack = aGeckoSessionCanGoBack; for (GeckoSession.NavigationDelegate listener : mNavigationListeners) { - listener.onCanGoBack(aSession, aCanGoBack); + listener.onCanGoBack(aSession, canGoBack()); } } @@ -856,12 +900,6 @@ public void onCanGoForward(@NonNull GeckoSession aSession, boolean aCanGoForward Log.d(LOGTAG, "onLoadRequest: " + uri); - String uriOverride = SessionUtils.checkYoutubeOverride(uri); - if (uriOverride != null) { - aSession.loadUri(uriOverride); - return GeckoResult.DENY; - } - if (aSession == mState.mSession) { Log.d(LOGTAG, "Testing for UA override"); @@ -910,11 +948,13 @@ public void onCanGoForward(@NonNull GeckoSession aSession, boolean aCanGoForward @Override public GeckoResult onNewSession(@NonNull GeckoSession aSession, @NonNull String aUri) { - Log.d(LOGTAG, "Session onStackSession: " + aUri); + mKeepAlive = System.currentTimeMillis() + KEEP_ALIVE_DURATION_MS; + Log.d(LOGTAG, "onNewSession: " + aUri); Session session = SessionStore.get().createSession(mState.mSettings, SESSION_DO_NOT_OPEN); session.mState.mParentId = mState.mId; - for (SessionChangeListener listener: new LinkedList<>(mSessionChangeListeners)) { + session.mKeepAlive = mKeepAlive; + for (SessionChangeListener listener: mSessionChangeListeners) { listener.onStackSession(session); } mSessionChangeListeners.add(session); @@ -925,7 +965,7 @@ public GeckoResult onNewSession(@NonNull GeckoSession aSession, @N public GeckoResult onLoadError(@NonNull GeckoSession session, String uri, @NonNull WebRequestError error) { Log.d(LOGTAG, "Session onLoadError: " + uri); - return GeckoResult.fromValue(InternalPages.createErrorPageDataURI(mContext, uri, error.category, error.code)); + return GeckoResult.fromValue(InternalPages.createErrorPageDataURI(mContext, uri, error.code)); } // Progress Listener @@ -938,6 +978,7 @@ public void onPageStart(@NonNull GeckoSession aSession, @NonNull String aUri) { Log.d(LOGTAG, "Session onPageStart"); mState.mIsLoading = true; TelemetryWrapper.startPageLoadTime(); + GleanMetricsService.startPageLoadTime(); for (GeckoSession.ProgressDelegate listener : mProgressListeners) { listener.onPageStart(aSession, aUri); @@ -953,6 +994,7 @@ public void onPageStop(@NonNull GeckoSession aSession, boolean b) { mState.mIsLoading = false; if (!SessionUtils.isLocalizedContent(mState.mUri)) { TelemetryWrapper.uploadPageLoadToHistogram(mState.mUri); + GleanMetricsService.stopPageLoadTimeWithURI(mState.mUri); } for (GeckoSession.ProgressDelegate listener : mProgressListeners) { @@ -1036,11 +1078,20 @@ public void onFirstComposite(@NonNull GeckoSession aSession) { for (GeckoSession.ContentDelegate listener : mContentListeners) { listener.onFirstComposite(aSession); } + if (mFirstContentfulPaint) { + // onFirstContentfulPaint is only called once after a session is opened. + // Notify onFirstContentfulPaint after a session is reattached before + // being closed ((e.g. tab selected) + for (GeckoSession.ContentDelegate listener : mContentListeners) { + listener.onFirstContentfulPaint(aSession); + } + } } } @Override public void onFirstContentfulPaint(@NonNull GeckoSession aSession) { + mFirstContentfulPaint = true; if (mState.mSession == aSession) { for (GeckoSession.ContentDelegate listener : mContentListeners) { listener.onFirstContentfulPaint(aSession); @@ -1380,10 +1431,10 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin // GeckoSession.SelectionActionDelegate @Override - public void onShowActionRequest(@NonNull GeckoSession aSession, @NonNull Selection selection, @NonNull String[] strings, @NonNull GeckoResponse geckoResponse) { + public void onShowActionRequest(@NonNull GeckoSession aSession, @NonNull Selection selection) { if (mState.mSession == aSession) { for (GeckoSession.SelectionActionDelegate listener : mSelectionActionListeners) { - listener.onShowActionRequest(aSession, selection, strings, geckoResponse); + listener.onShowActionRequest(aSession, selection); } } } 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 index a41270f60..7aae77b9b 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionSettings.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionSettings.java @@ -2,7 +2,8 @@ import android.content.Context; -import org.jetbrains.annotations.NotNull; +import androidx.annotation.NonNull; + import org.mozilla.geckoview.GeckoSessionSettings; import org.mozilla.vrbrowser.browser.SettingsStore; @@ -16,7 +17,7 @@ class SessionSettings { private boolean isServoEnabled; private String userAgentOverride; - /* package */ SessionSettings(@NotNull Builder builder) { + /* package */ SessionSettings(@NonNull Builder builder) { this.isPrivateBrowsingEnabled = builder.isPrivateBrowsingEnabled; this.isTrackingProtectionEnabled = builder.isTrackingProtectionEnabled; this.isSuspendMediaWhenInactiveEnabled = builder.isSuspendMediaWhenInactiveEnabled; 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 index 7615ec10c..3fea1e87e 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionState.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionState.java @@ -1,12 +1,7 @@ package org.mozilla.vrbrowser.browser.engine; -import android.graphics.Bitmap; - -import androidx.annotation.Nullable; - import com.google.gson.Gson; import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.JsonAdapter; @@ -25,7 +20,7 @@ @JsonAdapter(SessionState.SessionStateAdapterFactory.class) public class SessionState { - public transient boolean mIsActive; + private transient boolean mIsActive; public boolean mCanGoBack; public boolean mCanGoForward; public boolean mIsLoading; @@ -46,6 +41,21 @@ public class SessionState { public String mId = UUID.randomUUID().toString(); public String mParentId; // Parent session stack Id. + public SessionState recreate() { + SessionState result = new SessionState(); + result.mUri = mUri; + result.mPreviousUri = mPreviousUri; + result.mTitle = mTitle; + result.mSettings = mSettings; + result.mSessionState = mSessionState; + result.mLastUse = mLastUse; + result.mRegion = mRegion; + result.mId = mId; + result.mParentId = mParentId; + + return result; + } + public static class GeckoSessionStateAdapter extends TypeAdapter { @Override public void write(JsonWriter out, GeckoSession.SessionState session) throws IOException { @@ -64,6 +74,18 @@ public GeckoSession.SessionState read(JsonReader in) { } } + boolean isActive() { + return mIsActive; + } + + void setActive(boolean active) { + if (active == mIsActive) { + return; + } + mIsActive = active; + SessionStore.get().sessionActiveStateChanged(); + } + public class SessionStateAdapterFactory implements TypeAdapterFactory { public TypeAdapter create(Gson gson, TypeToken type) { final TypeAdapter delegate = gson.getDelegateAdapter(this, type); 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 index b67192897..250e50ea4 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionStore.java @@ -3,33 +3,28 @@ import android.content.Context; import android.content.res.Configuration; import android.os.Bundle; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.mozilla.geckoview.ContentBlocking; +import org.mozilla.gecko.util.ThreadUtils; 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.VRBrowserApplication; import org.mozilla.vrbrowser.browser.BookmarksStore; import org.mozilla.vrbrowser.browser.HistoryStore; import org.mozilla.vrbrowser.browser.PermissionDelegate; import org.mozilla.vrbrowser.browser.Services; -import org.mozilla.vrbrowser.browser.SettingsStore; -import org.mozilla.vrbrowser.crashreporting.CrashReporterService; +import org.mozilla.vrbrowser.utils.SystemUtils; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; public class SessionStore implements GeckoSession.PermissionDelegate { - - private static final String[] WEB_EXTENSIONS = new String[] { - "webcompat_vimeo", - "webcompat_youtube" - }; + private static final String LOGTAG = SystemUtils.createLogtag(SessionStore.class); + private static final int MAX_GECKO_SESSIONS = 5; private static SessionStore mInstance; @@ -48,6 +43,7 @@ public static SessionStore get() { private BookmarksStore mBookmarksStore; private HistoryStore mHistoryStore; private Services mServices; + private boolean mSuspendPending; private SessionStore() { mSessions = new ArrayList<>(); @@ -56,48 +52,10 @@ private SessionStore() { 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() - .antiTracking(ContentBlocking.AntiTracking.AD | ContentBlocking.AntiTracking.SOCIAL| ContentBlocking.AntiTracking.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()); - runtimeSettingsBuilder.autoplayDefault(SettingsStore.getInstance(mContext).isAutoplayEnabled() ? GeckoRuntimeSettings.AUTOPLAY_DEFAULT_ALLOWED : GeckoRuntimeSettings.AUTOPLAY_DEFAULT_BLOCKED); - - if (SettingsStore.getInstance(context).getTransparentBorderWidth() > 0) { - runtimeSettingsBuilder.useMaxScreenDepth(true); - } - - if (BuildConfig.DEBUG) { - runtimeSettingsBuilder.arguments(new String[] { "-purgecaches" }); - runtimeSettingsBuilder.debugLogging(true); - runtimeSettingsBuilder.aboutConfigEnabled(true); - } else { - runtimeSettingsBuilder.debugLogging(SettingsStore.getInstance(context).isDebugLogginEnabled()); - } - - 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); - } - } + // FIXME: Once GeckoView has a prefs API + SessionUtils.vrPrefsWorkAround(context, aExtras); - public GeckoRuntime getRuntime() { - return mRuntime; + mRuntime = EngineProvider.INSTANCE.getOrCreateRuntime(context); } public void initializeServices() { @@ -109,26 +67,32 @@ public void initializeStores(Context context) { mHistoryStore = new HistoryStore(context); } + @NonNull private Session addSession(@NonNull Session aSession) { aSession.setPermissionDelegate(this); aSession.addNavigationListener(mServices); mSessions.add(aSession); + sessionActiveStateChanged(); return aSession; } + @NonNull public Session createSession(boolean aPrivateMode) { SessionSettings settings = new SessionSettings(new SessionSettings.Builder().withDefaultSettings(mContext).withPrivateBrowsing(aPrivateMode)); return createSession(settings, Session.SESSION_OPEN); } + @NonNull /* package */ Session createSession(@NonNull SessionSettings aSettings, @Session.SessionOpenModeFlags int aOpenMode) { return addSession(new Session(mContext, mRuntime, aSettings, aOpenMode)); } + @NonNull public Session createSuspendedSession(SessionState aRestoreState) { return addSession(new Session(mContext, mRuntime, aRestoreState)); } + @NonNull public Session createSuspendedSession(final String aUri, final boolean aPrivateMode) { SessionState state = new SessionState(); state.mUri = aUri; @@ -173,9 +137,46 @@ public void suspendAllInactiveSessions() { } public void setActiveSession(Session aSession) { + if (aSession != null) { + aSession.setActive(true); + } mActiveSession = aSession; } + + private void limitInactiveSessions() { + Log.d(LOGTAG, "Limiting Inactive Sessions"); + suspendAllInactiveSessions(); + mSuspendPending = false; + } + + void sessionActiveStateChanged() { + if (mSuspendPending) { + return; + } + int count = 0; + int activeCount = 0; + int inactiveCount = 0; + int suspendedCount = 0; + for(Session session: mSessions) { + if (session.getGeckoSession() != null) { + count++; + if (session.isActive()) { + activeCount++; + } else { + inactiveCount++; + } + } else { + suspendedCount++; + } + } + if (count > MAX_GECKO_SESSIONS) { + Log.d(LOGTAG, "Too many GeckoSessions. Active: " + activeCount + " Inactive: " + inactiveCount + " Suspended: " + suspendedCount); + mSuspendPending = true; + ThreadUtils.postToUiThread(this::limitInactiveSessions); + } + } + public Session getActiveSession() { return mActiveSession; } @@ -183,7 +184,12 @@ public Session getActiveSession() { public ArrayList getSortedSessions(boolean aPrivateMode) { ArrayList result = new ArrayList<>(mSessions); result.removeIf(session -> session.isPrivateMode() != aPrivateMode); - result.sort((o1, o2) -> (int)(o2.getLastUse() - o1.getLastUse())); + result.sort((o1, o2) -> { + if (o2.getLastUse() < o1.getLastUse()) { + return -1; + } + return o2.getLastUse() == o1.getLastUse() ? 0 : 1; + }); return result; } @@ -277,22 +283,6 @@ public void setRemoteDebugging(final boolean 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; - } - - return false; - } - public void setLocales(List locales) { if (mRuntime != null) { mRuntime.getSettings().setLocales(locales.stream().toArray(String[]::new)); @@ -300,9 +290,19 @@ public void setLocales(List locales) { } public void clearCache(long clearFlags) { + LinkedList activeSession = new LinkedList<>(); for (Session session: mSessions) { - session.clearCache(clearFlags); + if (session.getGeckoSession() != null) { + session.suspend(); + activeSession.add(session); + } } + mRuntime.getStorageController().clearData(clearFlags).then(aVoid -> { + for (Session session: activeSession) { + session.recreateSession(); + } + return null; + }); } // Permission Delegate 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 index 11ac4bf99..62694a5eb 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/SessionUtils.java @@ -69,48 +69,4 @@ private static void addOptionalPref(FileOutputStream out, String aKey, Bundle aE 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/crashreporting/CrashReporterService.java b/app/src/common/shared/org/mozilla/vrbrowser/crashreporting/CrashReporterService.java index 16a217e36..275ba8255 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/crashreporting/CrashReporterService.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/crashreporting/CrashReporterService.java @@ -6,6 +6,9 @@ import android.os.Build; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.core.app.JobIntentService; + import org.mozilla.geckoview.GeckoRuntime; import org.mozilla.vrbrowser.BuildConfig; import org.mozilla.vrbrowser.R; @@ -13,15 +16,11 @@ import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.utils.SystemUtils; - import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.UUID; -import androidx.annotation.NonNull; -import androidx.core.app.JobIntentService; - public class CrashReporterService extends JobIntentService { private static final String LOGTAG = SystemUtils.createLogtag(CrashReporterService.class); @@ -128,7 +127,7 @@ protected void onHandleWork(@NonNull Intent intent) { Log.d(LOGTAG, "Content process crash " + intent); Intent broadcastIntent = new Intent(CRASH_ACTION); broadcastIntent.putExtra(DATA_TAG, intent); - sendBroadcast(broadcastIntent, getString(R.string.app_permission_name)); + sendBroadcast(broadcastIntent, BuildConfig.APPLICATION_ID + "." + getString(R.string.app_permission_name)); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/input/CustomKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/input/CustomKeyboard.java index dff537c65..815433dc8 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/input/CustomKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/input/CustomKeyboard.java @@ -20,13 +20,14 @@ public class CustomKeyboard extends Keyboard { private Key mEnterKey; private Key mSpaceKey; private Key mModeChangeKey; - private int mMaxColums; + private int mMaxColumns; private int[] mDisabledKeysIndexes; public static final int KEYCODE_SYMBOLS_CHANGE = -10; public static final int KEYCODE_VOICE_INPUT = -11; public static final int KEYCODE_LANGUAGE_CHANGE = -12; public static final int KEYCODE_EMOJI = -13; + public static final int KEYCODE_DOMAIN = -14; public CustomKeyboard(Context context, int xmlLayoutResId) { super(context, xmlLayoutResId, 0); @@ -36,7 +37,7 @@ public CustomKeyboard(Context context, int xmlLayoutResId) { public CustomKeyboard (Context context, int layoutTemplateResId, CharSequence characters, int columns, int horizontalPadding, int verticalGap) { this(context, layoutTemplateResId); - mMaxColums = columns; + mMaxColumns = columns; int x = 0; int y = 0; @@ -236,7 +237,7 @@ public int[] getShiftKeyIndices() { } public int getMaxColumns() { - return mMaxColums; + return mMaxColumns; } public void disableKeys(int[] disabledKeyIndexes) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/input/MotionEventGenerator.java b/app/src/common/shared/org/mozilla/vrbrowser/input/MotionEventGenerator.java index b8f2554fe..a3f000ece 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/input/MotionEventGenerator.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/input/MotionEventGenerator.java @@ -14,6 +14,9 @@ import org.mozilla.vrbrowser.ui.widgets.Widget; import org.mozilla.vrbrowser.utils.SystemUtils; +import java.util.Arrays; +import java.util.List; + public class MotionEventGenerator { static final String LOGTAG = SystemUtils.createLogtag(MotionEventGenerator.class); static class Device { @@ -24,6 +27,7 @@ static class Device { long mDownTime; MotionEvent.PointerProperties mProperties[]; MotionEvent.PointerCoords mCoords[]; + MotionEvent.PointerCoords mMouseOutCoords[]; Device(final int aDevice) { mDevice = aDevice; @@ -33,10 +37,16 @@ static class Device { mProperties[0].toolType = MotionEvent.TOOL_TYPE_FINGER; mCoords = new MotionEvent.PointerCoords[1]; mCoords[0] = new MotionEvent.PointerCoords(); - mCoords[0].toolMajor = 2; - mCoords[0].toolMinor = 2; - mCoords[0].touchMajor = 2; - mCoords[0].touchMinor = 2; + mMouseOutCoords = new MotionEvent.PointerCoords[1]; + for (MotionEvent.PointerCoords[] coords : Arrays.asList(mCoords, mMouseOutCoords)) { + coords[0] = new MotionEvent.PointerCoords(); + coords[0].toolMajor = 2; + coords[0].toolMinor = 2; + coords[0].touchMajor = 2; + coords[0].touchMinor = 2; + } + mMouseOutCoords[0].x = -10; + mMouseOutCoords[0].y = -10; } } @@ -44,13 +54,17 @@ static class Device { private static void generateEvent(Widget aWidget, Device aDevice, int aAction, boolean aGeneric) { + generateEvent(aWidget, aDevice, aAction, aGeneric, aDevice.mCoords); + } + + private static void generateEvent(Widget aWidget, Device aDevice, int aAction, boolean aGeneric, MotionEvent.PointerCoords[] aCoords) { MotionEvent event = MotionEvent.obtain( /*mDownTime*/ aDevice.mDownTime, /*eventTime*/ SystemClock.uptimeMillis(), /*action*/ aAction, /*pointerCount*/ 1, /*pointerProperties*/ aDevice.mProperties, - /*pointerCoords*/ aDevice.mCoords, + /*pointerCoords*/ aCoords, /*metaState*/ 0, /*buttonState*/ 0, /*xPrecision*/ 0, @@ -88,7 +102,7 @@ public static void dispatch(Widget aWidget, int aDevice, boolean aPressed, float generateEvent(device.mPreviousWidget, device, MotionEvent.ACTION_CANCEL, false); device.mWasPressed = false; } - generateEvent(device.mPreviousWidget, device, MotionEvent.ACTION_HOVER_EXIT, true); + generateEvent(device.mPreviousWidget, device, MotionEvent.ACTION_HOVER_EXIT, true, device.mMouseOutCoords); device.mPreviousWidget = null; } if (aWidget == null) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationLocalizationProvider.java b/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationLocalizationProvider.java index 500020da3..75d6cac1e 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationLocalizationProvider.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationLocalizationProvider.java @@ -1,9 +1,8 @@ package org.mozilla.vrbrowser.search; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.mozilla.vrbrowser.geolocation.GeolocationData; import java.util.Locale; @@ -26,7 +25,7 @@ public class GeolocationLocalizationProvider implements SearchLocalizationProvid @Nullable @Override - public SearchLocalization determineRegion(@NotNull Continuation continuation) { + public SearchLocalization determineRegion(@NonNull Continuation continuation) { return new SearchLocalization(mLanguage, mCountry, mRegion); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/search/SearchEngineWrapper.java b/app/src/common/shared/org/mozilla/vrbrowser/search/SearchEngineWrapper.java index a87c1b5c8..947185677 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/search/SearchEngineWrapper.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/search/SearchEngineWrapper.java @@ -125,6 +125,10 @@ public String getResourceURL() { return uri.getScheme() + "://" + uri.getHost(); } + public String getIdentifier() { + return mSearchEngine.getIdentifier(); + } + // Receiver for locale updates private BroadcastReceiver mLocaleChangedReceiver = new BroadcastReceiver() { @Override diff --git a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java index 5e124ad55..6e065a49c 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java @@ -3,13 +3,27 @@ import android.content.Context; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; +import org.mozilla.vrbrowser.BuildConfig; +import org.mozilla.vrbrowser.GleanMetrics.Distribution; +import org.mozilla.vrbrowser.GleanMetrics.FirefoxAccount; +import org.mozilla.vrbrowser.GleanMetrics.Pings; +import org.mozilla.vrbrowser.GleanMetrics.Searches; +import org.mozilla.vrbrowser.GleanMetrics.Url; +import org.mozilla.vrbrowser.GleanMetrics.Control; import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.search.SearchEngineWrapper; import org.mozilla.vrbrowser.utils.DeviceType; import org.mozilla.vrbrowser.utils.SystemUtils; -import org.mozilla.vrbrowser.BuildConfig; -import org.mozilla.vrbrowser.GleanMetrics.Distribution; +import org.mozilla.vrbrowser.utils.UrlUtils; + +import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; import mozilla.components.service.glean.Glean; import mozilla.components.service.glean.config.Configuration; @@ -18,9 +32,10 @@ public class GleanMetricsService { private final static String APP_NAME = "FirefoxReality"; - private static boolean initialized = false; private final static String LOGTAG = SystemUtils.createLogtag(GleanMetricsService.class); + private static boolean initialized = false; private static Context context = null; + private static HashSet domainMap = new HashSet(); // We should call this at the application initial stage. public static void init(Context aContext) { @@ -53,7 +68,155 @@ public static void stop() { Glean.INSTANCE.setUploadEnabled(false); } + public static void startPageLoadTime() { + // TODO: Blocked by Bug 1595914. + // pageLoadingTimerId = Pages.INSTANCE.getPageLoad().start(); + } + + public static void stopPageLoadTimeWithURI(String uri) { + // TODO: Blocked by Bug 1595914. + // Pages.INSTANCE.getPageLoad().stopAndAccumulate(pageLoadingTimerId); + + try { + URI uriLink = URI.create(uri); + if (uriLink.getHost() == null) { + return; + } + + if (domainMap.add(UrlUtils.stripCommonSubdomains(uriLink.getHost()))) { + Url.INSTANCE.getDomains().add(); + } + Url.INSTANCE.getVisits().add(); + + } catch (IllegalArgumentException e) { + Log.e(LOGTAG, "Invalid URL", e); + } + + } + + public static void sessionStop() { + domainMap.clear(); + Pings.INSTANCE.getSessionEnd().send(); + } + + @UiThread + public static void urlBarEvent(boolean aIsUrl) { + if (aIsUrl) { + Url.INSTANCE.getQueryType().get("type_link").add(); + } else { + Url.INSTANCE.getQueryType().get("type_query").add(); + // Record search engines. + String searchEngine = getDefaultSearchEngineIdentifierForTelemetry(); + Searches.INSTANCE.getCounts().get(searchEngine).add(); + } + } + + @UiThread + public static void voiceInputEvent() { + Url.INSTANCE.getQueryType().get("voice_query").add(); + + // Record search engines. + String searchEngine = getDefaultSearchEngineIdentifierForTelemetry(); + Searches.INSTANCE.getCounts().get(searchEngine).add(); + } + + public static void startImmersive() { + // TODO: Blocked by Bug 1595914 and 1595723. + // immersiveTimerId = Durarion.INSTANCE.getImmersiveMode().start(); + } + + public static void stopImmersive() { + // TODO: Blocked by Bug 1595914 and 1595723. + // Durarion.INSTANCE.getImmersiveMode().stopAndAccumulate(immersiveTimerId); + } + + // TODO: Confirm if we don't need multiple metrics for tracking window open duration. + // like WindowLifetime1 ~ WindowLifetimeN for multiple windows. + public static void openWindowEvent(int windowId) { + // TODO: Blocked by Bug 1595914 and Bug 1595723. + // GleanTimerId id = Durarion.INSTANCE.getWindowLifetime().start(); + // windowLifetimeId.put(windowId, id); + } + + public static void closeWindowEvent(int windowId) { + // TODO: Blocked by Bug 1595914 and Bug 1595723. + // if (windowLifetimeId.containsKey(windowId)) { + // Durarion.INSTANCE.getWindowLifetime().stopAndAccumulate(windowLifetimeId.get(windowId)); + // windowLifetimeId.remove(windowId); + // } else { + // Log.e(LOGTAG, "Can't find window id."); + // } + } + + private static String getDefaultSearchEngineIdentifierForTelemetry() { + return SearchEngineWrapper.get(context).getIdentifier(); + } + + public static void newWindowOpenEvent() { + Control.INSTANCE.getOpenNewWindow().add(); + } + private static void setStartupMetrics() { Distribution.INSTANCE.getChannelName().set(DeviceType.isOculusBuild() ? "oculusvr" : BuildConfig.FLAVOR_platform); } + + @VisibleForTesting + public static void testSetStartupMetrics() { + setStartupMetrics(); + } + + public static class FxA { + + public static void signIn() { + FirefoxAccount.INSTANCE.getSignIn().record(); + } + + public static void signInResult(boolean status) { + Map map = new HashMap<>(); + map.put(FirefoxAccount.signInResultKeys.state, String.valueOf(status)); + FirefoxAccount.INSTANCE.getSignInResult().record(map); + } + + public static void signOut() { + FirefoxAccount.INSTANCE.getSignOut().record(); + } + + public static void bookmarksSyncStatus(boolean status) { + FirefoxAccount.INSTANCE.getBookmarksSyncStatus().set(status); + } + + public static void historySyncStatus(boolean status) { + FirefoxAccount.INSTANCE.getHistorySyncStatus().set(status); + } + + public static void sentTab() { + FirefoxAccount.INSTANCE.getTabSent().add(); + } + + public static void receivedTab(@NonNull mozilla.components.concept.sync.DeviceType source) { + FirefoxAccount.INSTANCE.getReceivedTab().get(source.name()).add(); + } + } + + public static class Tabs { + + public enum TabSource { + CONTEXT_MENU, // Tab opened from the browsers long click context menu + TABS_DIALOG, // Tab opened from the tabs dialog + BOOKMARKS, // Tab opened from the bookmarks panel + HISTORY, // Tab opened from the history panel + FXA_LOGIN, // Tab opened by the FxA login flow + RECEIVED, // Tab opened by FxA when a tab is received + PRE_EXISTING, // Tab opened as a result of restoring the last session + BROWSER // Tab opened by the browser as a result of a new window open + } + + public static void openedCounter(@NonNull TabSource source) { + org.mozilla.vrbrowser.GleanMetrics.Tabs.INSTANCE.getOpened().get(source.name()).add(); + } + + public static void activatedEvent() { + org.mozilla.vrbrowser.GleanMetrics.Tabs.INSTANCE.getActivated().add(); + } + } } 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 574c73667..79dbaa136 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java @@ -91,26 +91,18 @@ private class Method { private static final String IMMERSIVE_MODE = "immersive_mode"; private static final String TELEMETRY_STATUS = "status"; - // 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"; + // When a session is multi-window, what time is each position the active window + private static final String PLACEMENTS_ACTIVE_TIME = "place_active_time"; + // When a session is multi-window, what time are one, two or three windows open + private static final String OPEN_WINDOWS_TIME = "open_windows_time"; + // The weight of windows are opened at a time, per session + private static final String WINDOWS_OPEN_W = "windows_open_w"; } private class Object { @@ -119,22 +111,25 @@ private class Object { 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"; + private static final String WINDOW_MOVES_COUNT = "windows_moves_count"; + private static final String WINDOW_RESIZE_COUNT = "windows_resize_count"; + private static final String LEFT_WINDOW_ACTIVE_TIME = "left_window_active_time"; + private static final String FRONT_WINDOW_ACTIVE_TIME = "front_window_active_time"; + private static final String RIGHT_WINDOW_ACTIVE_TIME = "right_window_active_time"; + private static final String ONE_OPEN_WINDOWS_TIME = "one_w_open_time"; + private static final String TWO_OPEN_WINDOWS_TIME = "two_w_open_time"; + private static final String THREE_OPEN_WINDOWS_TIME = "thr_w_open_time"; + private static final String ONE_WINDOWS_OPENED = "one_w_open"; + private static final String TWO_WINDOWS_OPENED = "two_w_open"; + private static final String THREE_WINDOWS_OPENED = "thr_w_open"; + private static final String ONE_PRIVATE_WINDOWS_OPENED = "one_pri_w_open"; + private static final String TWO_PRIVATE_WINDOWS_OPENED = "two_pri_w_open"; + private static final String THREE_PRIVATE_WINDOWS_OPENED = "thr_pri_w_open"; private static final String TELEMETRY_STATUS = "telemetry_status"; } @@ -403,35 +398,14 @@ public static void queueMultiWindowEvents() { // Queue Windows resizes freq during the session queueWindowsResizesCountEvent(); - // Queue Windows active time pct - queueActiveWindowPctEvent(); + // Queue Windows active time. + queueActiveWindowTimeEvent(); - // Queue Windows active time pct - queueOpenWindowsAvgEvent(); + // Queue Windows active weight. + queueOpenWindowsWeightEvent(); // 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"); + queueOpenWindowsTimeEvent(); } public static void openWindowEvent(int windowId) { @@ -509,20 +483,13 @@ public static void openWindowsEvent(int from, int to, boolean isPrivate) { 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, "Open 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()); @@ -538,28 +505,26 @@ private static void queueWindowsLifetimeHistogram() { } 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.extra(Extra.WINDOW_MOVES_COUNT, Integer.toString(windowsMovesCount)); event.queue(); - Log.d(LOGTAG, "[Queue] Windows Moves Freq: " + movesFreqPerSession); + Log.d(LOGTAG, "[Queue] Windows Moves per session: " + windowsMovesCount); 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.extra(Extra.WINDOW_RESIZE_COUNT, Integer.toString(windowsResizesCount)); event.queue(); - Log.d(LOGTAG, "[Queue] Windows Resizes Freq: " + resizesFreqPerSession); + Log.d(LOGTAG, "[Queue] Windows Resizes per session: " + windowsResizesCount); windowsResizesCount = 0; } - private static void queueActiveWindowPctEvent() { + private static void queueActiveWindowTimeEvent() { for (int index = 0; index< MAX_WINDOWS; index++) { if (activePlacementStartTime[index] != 0) { activePlacementTime[index] += SystemClock.elapsedRealtime() - activePlacementStartTime[index]; @@ -567,59 +532,61 @@ private static void queueActiveWindowPctEvent() { } } - 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()])); + TelemetryEvent event = TelemetryEvent.create(Category.ACTION, Method.PLACEMENTS_ACTIVE_TIME, Object.WINDOW); + event.extra(Extra.LEFT_WINDOW_ACTIVE_TIME, String.valueOf(activePlacementTime[WindowPlacement.LEFT.getValue()])); + event.extra(Extra.FRONT_WINDOW_ACTIVE_TIME, String.valueOf(activePlacementTime[WindowPlacement.FRONT.getValue()])); + event.extra(Extra.RIGHT_WINDOW_ACTIVE_TIME, String.valueOf(activePlacementTime[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()]); + Log.d(LOGTAG, "[Queue] Placements Active time total:"); + Log.d(LOGTAG, "\tFRONT: " + activePlacementTime[WindowPlacement.FRONT.getValue()]); + Log.d(LOGTAG, "\tLEFT: " + activePlacementTime[WindowPlacement.LEFT.getValue()]); + Log.d(LOGTAG, "\tRIGHT: " + activePlacementTime[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; + public static void resetOpenedWindowsCount(int number, boolean isPrivate) { + if (isPrivate) { + for (int i=0; i 0) { + openPrivateWindows[number-1] = 1; + } + + } else { + for (int i=0; i 0) { + openWindows[number-1] = 1; + } } - float privateWM = weightSum > 0 ? weightSum/weight : 0; + } + + private static void queueOpenWindowsWeightEvent() { + TelemetryEvent event = TelemetryEvent.create(Category.ACTION, Method.WINDOWS_OPEN_W, Object.WINDOW); + event.extra(Extra.ONE_WINDOWS_OPENED, String.valueOf(openWindows[0])); + event.extra(Extra.TWO_WINDOWS_OPENED, String.valueOf(openWindows[1])); + event.extra(Extra.THREE_WINDOWS_OPENED, String.valueOf(openWindows[2])); + event.extra(Extra.ONE_PRIVATE_WINDOWS_OPENED, String.valueOf(openPrivateWindows[0])); + event.extra(Extra.TWO_PRIVATE_WINDOWS_OPENED, String.valueOf(openPrivateWindows[1])); + event.extra(Extra.THREE_PRIVATE_WINDOWS_OPENED, String.valueOf(openPrivateWindows[2])); - 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); + Log.d(LOGTAG, "[Queue] Open Windows Number:"); + Log.d(LOGTAG, "\tRegular 1: " + openWindows[0]); + Log.d(LOGTAG, "\tRegular 2: " + openWindows[1]); + Log.d(LOGTAG, "\tRegular 3: " + openWindows[2]); + + Log.d(LOGTAG, "\tPrivate 1: " + openPrivateWindows[0]); + Log.d(LOGTAG, "\tPrivate 2: " + openPrivateWindows[1]); + Log.d(LOGTAG, "\tPrivate 3: " + openPrivateWindows[2]); for (int index = 0; index< MAX_WINDOWS; index++) { if (openWindows[index] != 0) { @@ -631,7 +598,7 @@ private static void queueOpenWindowsAvgEvent() { } } - private static void queueOpenWindowsPctEvent() { + private static void queueOpenWindowsTimeEvent() { for (int index = 0; index< MAX_WINDOWS; index++) { if (openWindowsStartTime[index] != 0) { openWindowsTime[index] += SystemClock.elapsedRealtime() - openWindowsStartTime[index]; @@ -644,30 +611,16 @@ private static void queueOpenWindowsPctEvent() { } } - 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()])); + TelemetryEvent event = TelemetryEvent.create(Category.ACTION, Method.OPEN_WINDOWS_TIME, Object.WINDOW); + event.extra(Extra.ONE_OPEN_WINDOWS_TIME, String.valueOf(openWindowsTime[0]+openPrivateWindowsTime[0])); + event.extra(Extra.TWO_OPEN_WINDOWS_TIME, String.valueOf(openWindowsTime[1]+openPrivateWindowsTime[1])); + event.extra(Extra.THREE_OPEN_WINDOWS_TIME, String.valueOf(openWindowsTime[2]+openPrivateWindowsTime[2])); 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()]); + Log.d(LOGTAG, "[Queue] Open Windows time:"); + Log.d(LOGTAG, "\tONE: " + String.valueOf(openWindowsTime[0] + openPrivateWindowsTime[0])); + Log.d(LOGTAG, "\tTWO: " + String.valueOf(openWindowsTime[1] + openPrivateWindowsTime[1])); + Log.d(LOGTAG, "\tTHREE: " + String.valueOf(openWindowsTime[2] + openPrivateWindowsTime[2])); for (int index = 0; index< MAX_WINDOWS; index++) { openWindowsTime[index] = 0; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/Bookmark.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/Bookmark.java index 4c411499d..281ea2f13 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/Bookmark.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/Bookmark.java @@ -104,7 +104,8 @@ private static List getDisplayListTree(@NonNull List boo for (BookmarkNode node : bookmarkNodes) { if (node.getType() == BookmarkNodeType.FOLDER) { if (openFolderGuid != null && openFolderGuid.contains(node.getGuid())) { - Bookmark bookmark = new Bookmark(node, level, true); + boolean canExpand = node.getChildren() != null && !node.getChildren().isEmpty(); + Bookmark bookmark = new Bookmark(node, level, canExpand); children.add(bookmark); if (node.getChildren() != null) { children.addAll(getDisplayListTree(node.getChildren(), level + 1, openFolderGuid)); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BookmarkAdapter.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BookmarkAdapter.java index 57a47e5ba..599b3b4a4 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BookmarkAdapter.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BookmarkAdapter.java @@ -24,7 +24,6 @@ import org.mozilla.vrbrowser.utils.AnimationHelper; import org.mozilla.vrbrowser.utils.SystemUtils; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -44,8 +43,6 @@ public class BookmarkAdapter extends RecyclerView.Adapter { + binding.setIsHovered(true); int ev = motionEvent.getActionMasked(); switch (ev) { case MotionEvent.ACTION_UP: - binding.setIsHovered(true); if (mBookmarkItemCallback != null) { mBookmarkItemCallback.onMore(view, binding.getItem()); } + binding.more.setImageState(new int[]{android.R.attr.state_active},true); return true; case MotionEvent.ACTION_DOWN: - binding.setIsHovered(true); + binding.more.setImageState(new int[]{android.R.attr.state_pressed},true); return true; + + case MotionEvent.ACTION_CANCEL: + binding.setIsHovered(false); + binding.more.setImageState(new int[]{android.R.attr.state_active},true); + return false; } return false; }); binding.trash.setOnHoverListener(mIconHoverListener); binding.trash.setOnTouchListener((view, motionEvent) -> { + binding.setIsHovered(true); int ev = motionEvent.getActionMasked(); switch (ev) { case MotionEvent.ACTION_UP: - binding.setIsHovered(true); if (mBookmarkItemCallback != null) { mBookmarkItemCallback.onDelete(view, binding.getItem()); } + binding.trash.setImageState(new int[]{android.R.attr.state_active},true); return true; case MotionEvent.ACTION_DOWN: - binding.setIsHovered(true); + binding.trash.setImageState(new int[]{android.R.attr.state_pressed},true); return true; + + case MotionEvent.ACTION_CANCEL: + binding.setIsHovered(false); + binding.trash.setImageState(new int[]{android.R.attr.state_active},true); + return false; } return false; }); @@ -324,7 +332,7 @@ static class BookmarkSeparatorViewHolder extends RecyclerView.ViewHolder { int ev = motionEvent.getActionMasked(); switch (ev) { case MotionEvent.ACTION_HOVER_ENTER: - icon.setColorFilter(mIconColorHover); + icon.setImageState(new int[]{android.R.attr.state_hovered},true); AnimationHelper.animateViewPadding(view, mMaxPadding, mMinPadding, @@ -332,11 +340,11 @@ static class BookmarkSeparatorViewHolder extends RecyclerView.ViewHolder { return false; case MotionEvent.ACTION_HOVER_EXIT: + icon.setImageState(new int[]{android.R.attr.state_active},true); AnimationHelper.animateViewPadding(view, mMinPadding, mMaxPadding, - ICON_ANIMATION_DURATION, - () -> icon.setColorFilter(mIconNormalColor)); + ICON_ANIMATION_DURATION); return false; } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/HistoryAdapter.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/HistoryAdapter.java index 92ad5fb07..a2a2fef73 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/HistoryAdapter.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/HistoryAdapter.java @@ -41,8 +41,6 @@ public class HistoryAdapter extends RecyclerView.Adapter { + binding.setIsHovered(true); int ev = motionEvent.getActionMasked(); switch (ev) { case MotionEvent.ACTION_UP: - binding.setIsHovered(true); if (mHistoryItemCallback != null) { mHistoryItemCallback.onMore(view, binding.getItem()); } + binding.more.setImageState(new int[]{android.R.attr.state_active},true); return true; case MotionEvent.ACTION_DOWN: - binding.setIsHovered(true); + binding.more.setImageState(new int[]{android.R.attr.state_pressed},true); return true; + + case MotionEvent.ACTION_CANCEL: + binding.setIsHovered(false); + binding.more.setImageState(new int[]{android.R.attr.state_active},true); + return false; } return false; }); binding.trash.setOnHoverListener(mIconHoverListener); binding.trash.setOnTouchListener((view, motionEvent) -> { + binding.setIsHovered(true); int ev = motionEvent.getActionMasked(); switch (ev) { case MotionEvent.ACTION_UP: - binding.setIsHovered(true); if (mHistoryItemCallback != null) { mHistoryItemCallback.onDelete(view, binding.getItem()); } + binding.trash.setImageState(new int[]{android.R.attr.state_active},true); return true; case MotionEvent.ACTION_DOWN: - binding.setIsHovered(true); + binding.trash.setImageState(new int[]{android.R.attr.state_pressed},true); return true; + + case MotionEvent.ACTION_CANCEL: + binding.setIsHovered(false); + binding.trash.setImageState(new int[]{android.R.attr.state_active},true); + return false; } return false; }); @@ -261,7 +270,7 @@ private boolean isPositionHeader(int position) { int ev = motionEvent.getActionMasked(); switch (ev) { case MotionEvent.ACTION_HOVER_ENTER: - icon.setColorFilter(mIconColorHover); + icon.setImageState(new int[]{android.R.attr.state_hovered},true); AnimationHelper.animateViewPadding(view, mMaxPadding, mMinPadding, @@ -269,11 +278,11 @@ private boolean isPositionHeader(int position) { return false; case MotionEvent.ACTION_HOVER_EXIT: + icon.setImageState(new int[]{android.R.attr.state_active},true); AnimationHelper.animateViewPadding(view, mMinPadding, mMaxPadding, - ICON_ANIMATION_DURATION, - () -> icon.setColorFilter(mIconNormalColor)); + ICON_ANIMATION_DURATION); return false; } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/LanguagesAdapter.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/LanguagesAdapter.java index 9a707b738..78527c6b7 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/LanguagesAdapter.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/LanguagesAdapter.java @@ -1,11 +1,14 @@ package org.mozilla.vrbrowser.ui.adapters; +import android.animation.ValueAnimator; import android.annotation.SuppressLint; +import android.content.Context; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; +import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -23,16 +26,29 @@ public class LanguagesAdapter extends RecyclerView.Adapter { + private static final int ICON_ANIMATION_DURATION = 200; + private List mLanguagesList; private boolean mIsPreferred; + private int mIconColorHover; + private int mIconNormalColor; + private int mIconSize; + private int mMaxIconSize; + @Nullable private final LanguageItemCallback mLanguageItemCallback; - public LanguagesAdapter(@Nullable LanguageItemCallback clickCallback, boolean isPreferred) { + public LanguagesAdapter(@NonNull Context context, @Nullable LanguageItemCallback clickCallback, boolean isPreferred) { mLanguageItemCallback = clickCallback; mIsPreferred = isPreferred; + mIconSize = (int)context.getResources().getDimension(R.dimen.language_row_icon_size); + mMaxIconSize = mIconSize + ((mIconSize*25)/100); + + mIconColorHover = context.getResources().getColor(R.color.smoke, context.getTheme()); + mIconNormalColor = context.getResources().getColor(R.color.concrete, context.getTheme()); + setHasStableIds(true); } @@ -123,6 +139,10 @@ public LanguageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int view LanguageItemBinding binding = DataBindingUtil .inflate(LayoutInflater.from(parent.getContext()), R.layout.language_item, parent, false); + binding.add.setOnHoverListener(mIconHoverListener); + binding.delete.setOnHoverListener(mIconHoverListener); + binding.up.setOnHoverListener(mIconHoverListener); + binding.down.setOnHoverListener(mIconHoverListener); binding.setIsPreferred(mIsPreferred); return new LanguageViewHolder(binding); @@ -187,4 +207,44 @@ public long getItemId(int position) { return position; } + private View.OnHoverListener mIconHoverListener = (view, motionEvent) -> { + ImageView icon = (ImageView)view; + int ev = motionEvent.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_HOVER_ENTER: { + icon.setColorFilter(mIconColorHover); + ValueAnimator anim = ValueAnimator.ofInt(mIconSize, mMaxIconSize); + anim.addUpdateListener(valueAnimator -> { + int val = (Integer) valueAnimator.getAnimatedValue(); + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + layoutParams.width = val; + layoutParams.height = val; + view.setLayoutParams(layoutParams); + }); + anim.setDuration(ICON_ANIMATION_DURATION); + anim.start(); + + return false; + } + + case MotionEvent.ACTION_HOVER_EXIT: { + ValueAnimator anim = ValueAnimator.ofInt(mMaxIconSize, mIconSize); + anim.addUpdateListener(valueAnimator -> { + int val = (Integer) valueAnimator.getAnimatedValue(); + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + layoutParams.width = val; + layoutParams.height = val; + view.setLayoutParams(layoutParams); + }); + anim.setDuration(ICON_ANIMATION_DURATION); + anim.start(); + icon.setColorFilter(mIconNormalColor); + + return false; + } + } + + return false; + }; + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/PopUpAdapter.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/PopUpAdapter.java index 888ab4661..f8d0e4a85 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/PopUpAdapter.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/PopUpAdapter.java @@ -1,9 +1,12 @@ package org.mozilla.vrbrowser.ui.adapters; +import android.animation.ValueAnimator; import android.content.Context; import android.view.LayoutInflater; import android.view.MotionEvent; +import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; @@ -24,13 +27,26 @@ public class PopUpAdapter extends RecyclerView.Adapter static final String LOGTAG = SystemUtils.createLogtag(BookmarkAdapter.class); + private static final int ICON_ANIMATION_DURATION = 200; + private List mDisplayList; private PopUpSiteItemCallback mCallback; + private int mIconColorHover; + private int mIconNormalColor; + private int mIconSize; + private int mMaxIconSize; + public PopUpAdapter(Context aContext, PopUpSiteItemCallback callback) { mCallback = callback; + mIconSize = (int)aContext.getResources().getDimension(R.dimen.language_row_icon_size); + mMaxIconSize = mIconSize + ((mIconSize*25)/100); + + mIconColorHover = aContext.getResources().getColor(R.color.smoke, aContext.getTheme()); + mIconNormalColor = aContext.getResources().getColor(R.color.concrete, aContext.getTheme()); + setHasStableIds(true); } @@ -93,6 +109,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int return false; }); + binding.delete.setOnHoverListener(mIconHoverListener); return new PopUpSiteViewHolder(binding); } @@ -119,5 +136,45 @@ static class PopUpSiteViewHolder extends RecyclerView.ViewHolder { } } + private View.OnHoverListener mIconHoverListener = (view, motionEvent) -> { + ImageView icon = (ImageView)view; + int ev = motionEvent.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_HOVER_ENTER: { + icon.setColorFilter(mIconColorHover); + ValueAnimator anim = ValueAnimator.ofInt(mIconSize, mMaxIconSize); + anim.addUpdateListener(valueAnimator -> { + int val = (Integer) valueAnimator.getAnimatedValue(); + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + layoutParams.width = val; + layoutParams.height = val; + view.setLayoutParams(layoutParams); + }); + anim.setDuration(ICON_ANIMATION_DURATION); + anim.start(); + + return false; + } + + case MotionEvent.ACTION_HOVER_EXIT: { + ValueAnimator anim = ValueAnimator.ofInt(mMaxIconSize, mIconSize); + anim.addUpdateListener(valueAnimator -> { + int val = (Integer) valueAnimator.getAnimatedValue(); + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + layoutParams.width = val; + layoutParams.height = val; + view.setLayoutParams(layoutParams); + }); + anim.setDuration(ICON_ANIMATION_DURATION); + anim.start(); + icon.setColorFilter(mIconNormalColor); + + return false; + } + } + + return false; + }; + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarksCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarksCallback.java index 9049e3318..26f0002ae 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarksCallback.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarksCallback.java @@ -12,4 +12,6 @@ default void onSyncBookmarks(@NonNull View view) {} default void onFxALogin(@NonNull View view) {} default void onFxASynSettings(@NonNull View view) {} default void onShowContextMenu(@NonNull View view, Bookmark item, boolean isLastVisibleItem) {} + default void onHideContextMenu(@NonNull View view) {} + default void onClickItem(@NonNull View view, Bookmark item) {} } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryCallback.java index 60cc88652..6125ba8b1 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryCallback.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryCallback.java @@ -12,4 +12,6 @@ default void onSyncHistory(@NonNull View view) {} default void onFxALogin(@NonNull View view) {} default void onFxASynSettings(@NonNull View view) {} default void onShowContextMenu(@NonNull View view, @NonNull VisitInfo item, boolean isLastVisibleItem) {} + default void onHideContextMenu(@NonNull View view) {} + default void onClickItem(@NonNull View view, @NonNull VisitInfo item) {} } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/BaseKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/BaseKeyboard.java index 4e75ec626..eaf3b89ff 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/BaseKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/BaseKeyboard.java @@ -9,6 +9,7 @@ import java.util.Locale; import java.util.regex.Pattern; +import java.util.stream.Stream; public abstract class BaseKeyboard implements KeyboardInterface { protected Context mContext; @@ -51,4 +52,10 @@ public String getModeChangeKeyText() { public float getAlphabeticKeyboardWidth() { return WidgetPlacement.dpDimension(mContext, R.dimen.keyboard_alphabetic_width); } + + @Override + public String[] getDomains(String... domains) { + return Stream.of(new String[]{".com", ".net", ".org", ".co"}, domains).flatMap(Stream::of) + .toArray(String[]::new); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/DanishKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/DanishKeyboard.java index deac264e8..f3673c316 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/DanishKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/DanishKeyboard.java @@ -65,4 +65,9 @@ public Locale getLocale() { public String getSpaceKeyText(String aComposingText) { return StringUtils.getStringByLocale(mContext, R.string.settings_language_danish, getLocale()); } + + @Override + public String[] getDomains(String... domains) { + return super.getDomains(".dk"); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/DutchKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/DutchKeyboard.java index a92124457..1b3bc0e9f 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/DutchKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/DutchKeyboard.java @@ -65,4 +65,9 @@ public Locale getLocale() { public String getSpaceKeyText(String aComposingText) { return StringUtils.getStringByLocale(mContext, R.string.settings_language_dutch, getLocale()); } + + @Override + public String[] getDomains(String... domains) { + return super.getDomains(".nl"); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/EnglishKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/EnglishKeyboard.java index 41d9835da..9c753d853 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/EnglishKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/EnglishKeyboard.java @@ -10,6 +10,7 @@ import androidx.annotation.Nullable; import java.util.Locale; +import java.util.stream.Stream; public class EnglishKeyboard extends BaseKeyboard { private CustomKeyboard mKeyboard; @@ -47,4 +48,9 @@ public String getSpaceKeyText(String aComposingText) { return StringUtils.getStringByLocale(mContext, R.string.settings_language_english, getLocale()); } + + @Override + public String[] getDomains(String... domains) { + return super.getDomains(".uk", ".us"); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/FinnishKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/FinnishKeyboard.java index 653cc9601..cdefee3bb 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/FinnishKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/FinnishKeyboard.java @@ -65,4 +65,9 @@ public Locale getLocale() { public String getSpaceKeyText(String aComposingText) { return StringUtils.getStringByLocale(mContext, R.string.settings_language_finnish, getLocale()); } + + @Override + public String[] getDomains(String... domains) { + return super.getDomains(".fi"); + } } \ No newline at end of file diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/FrenchKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/FrenchKeyboard.java index bfe133828..9a9d20b46 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/FrenchKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/FrenchKeyboard.java @@ -47,4 +47,9 @@ public Locale getLocale() { public String getSpaceKeyText(String aComposingText) { return StringUtils.getStringByLocale(mContext, R.string.settings_language_french, getLocale()); } + + @Override + public String[] getDomains(String... domains) { + return super.getDomains(".fr"); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/GermanKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/GermanKeyboard.java index fde18ad97..f990636b6 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/GermanKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/GermanKeyboard.java @@ -63,4 +63,9 @@ public Locale getLocale() { public String getSpaceKeyText(String aComposingText) { return StringUtils.getStringByLocale(mContext, R.string.settings_language_german, getLocale()); } + + @Override + public String[] getDomains(String... domains) { + return super.getDomains(".de", ".at", ".ch"); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/ItalianKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/ItalianKeyboard.java index 3b1e09003..9ff942c16 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/ItalianKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/ItalianKeyboard.java @@ -47,4 +47,9 @@ public Locale getLocale() { public String getSpaceKeyText(String aComposingText) { return StringUtils.getStringByLocale(mContext, R.string.settings_language_italian, getLocale()); } + + @Override + public String[] getDomains(String... domains) { + return super.getDomains(".it"); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/JapaneseKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/JapaneseKeyboard.java index 72496abd7..41f279ebe 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/JapaneseKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/JapaneseKeyboard.java @@ -208,4 +208,9 @@ public void clear() { mConverter.init(); } + @Override + public String[] getDomains(String... domains) { + return super.getDomains(".jp"); + } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/KeyboardInterface.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/KeyboardInterface.java index 7f1a25063..8af9cf0de 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/KeyboardInterface.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/KeyboardInterface.java @@ -40,6 +40,7 @@ public enum Action { default boolean usesTextOverride() { return false; } String getComposingText(String aComposing, String aCode); String getKeyboardTitle(); + default String[] getDomains(String... domains) { return null; } Locale getLocale(); String getSpaceKeyText(String aComposingText); String getEnterKeyText(int aIMEOptions, String aComposingText); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/KoreanKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/KoreanKeyboard.java index e6916bdc2..a18a119bd 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/KoreanKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/KoreanKeyboard.java @@ -405,4 +405,9 @@ public Locale getLocale() { public String getSpaceKeyText(String aComposingText) { return StringUtils.getStringByLocale(mContext, R.string.settings_language_korean, getLocale()); } + + @Override + public String[] getDomains(String... domains) { + return super.getDomains(".kr"); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/NorwegianKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/NorwegianKeyboard.java index 601aa3bf6..3add97cc5 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/NorwegianKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/NorwegianKeyboard.java @@ -65,4 +65,9 @@ public Locale getLocale() { public String getSpaceKeyText(String aComposingText) { return StringUtils.getStringByLocale(mContext, R.string.settings_language_norwegian, getLocale()); } + + @Override + public String[] getDomains(String... domains) { + return super.getDomains(".no"); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/PolishKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/PolishKeyboard.java index 241f98027..97fe3e4d9 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/PolishKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/PolishKeyboard.java @@ -65,4 +65,9 @@ public Locale getLocale() { public String getSpaceKeyText(String aComposingText) { return StringUtils.getStringByLocale(mContext, R.string.settings_language_polish, getLocale()); } + + @Override + public String[] getDomains(String... domains) { + return super.getDomains(".pl"); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/SwedishKeyboard.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/SwedishKeyboard.java index f011de4bb..b3a9edbbc 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/SwedishKeyboard.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/keyboards/SwedishKeyboard.java @@ -65,4 +65,9 @@ public Locale getLocale() { public String getSpaceKeyText(String aComposingText) { return StringUtils.getStringByLocale(mContext, R.string.settings_language_swedish, getLocale()); } + + @Override + public String[] getDomains(String... domains) { + return super.getDomains(".se"); + } } 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 06862f872..cd82489b6 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 @@ -14,12 +14,11 @@ import android.widget.FrameLayout; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.databinding.DataBindingUtil; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.mozilla.geckoview.GeckoSessionSettings; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.VRBrowserActivity; @@ -30,6 +29,7 @@ import org.mozilla.vrbrowser.browser.engine.Session; import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.databinding.BookmarksBinding; +import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.ui.adapters.Bookmark; import org.mozilla.vrbrowser.ui.adapters.BookmarkAdapter; import org.mozilla.vrbrowser.ui.adapters.CustomLinearLayoutManager; @@ -40,6 +40,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import mozilla.appservices.places.BookmarkRoot; @@ -97,6 +98,7 @@ private void initialize(Context aContext) { v.requestFocusFromTouch(); return false; }); + mBinding.bookmarksList.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> mBookmarksViewListeners.forEach((listener) -> listener.onHideContextMenu(v))); mBinding.bookmarksList.setHasFixedSize(true); mBinding.bookmarksList.setItemViewCacheSize(20); mBinding.bookmarksList.setDrawingCacheEnabled(true); @@ -154,19 +156,14 @@ public void onClick(@NonNull View view, @NonNull Bookmark item) { Session session = SessionStore.get().getActiveSession(); session.loadUri(item.getUrl()); + + mBookmarksViewListeners.forEach((listener) -> listener.onClickItem(view, item)); } @Override public void onDelete(@NonNull View view, @NonNull Bookmark item) { mBinding.bookmarksList.requestFocusFromTouch(); - mBookmarkAdapter.removeItem(item); - if (mBookmarkAdapter.itemCount() == 0) { - mBinding.setIsEmpty(true); - mBinding.setIsLoading(false); - mBinding.executePendingBindings(); - } - SessionStore.get().getBookmarkStore().deleteBookmarkById(item.getGuid()); } @@ -211,18 +208,33 @@ public void onSyncBookmarks(@NonNull View view) { @Override public void onFxALogin(@NonNull View view) { - mAccounts.getAuthenticationUrlAsync().thenAcceptAsync((url) -> { - if (url != null) { - mAccounts.setLoginOrigin(Accounts.LoginOrigin.BOOKMARKS); - WidgetManagerDelegate widgetManager = ((VRBrowserActivity)getContext()); - widgetManager.openNewTabForeground(url); - widgetManager.getFocusedWindow().getSession().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_MOBILE); + if (mAccounts.getAccountStatus() == Accounts.AccountStatus.SIGNED_IN) { + mAccounts.logoutAsync(); + + } else { + CompletableFuture result = mAccounts.authUrlAsync(); + if (result != null) { + result.thenAcceptAsync((url) -> { + if (url == null) { + mAccounts.logoutAsync(); + + } else { + mAccounts.setLoginOrigin(Accounts.LoginOrigin.BOOKMARKS); + WidgetManagerDelegate widgetManager = ((VRBrowserActivity) getContext()); + widgetManager.openNewTabForeground(url); + widgetManager.getFocusedWindow().getSession().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_MOBILE); + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.FXA_LOGIN); + + mBookmarksViewListeners.forEach((listener) -> listener.onFxALogin(view)); + } + + }, mUIThreadExecutor).exceptionally(throwable -> { + Log.d(LOGTAG, "Error getting the authentication URL: " + throwable.getLocalizedMessage()); + throwable.printStackTrace(); + return null; + }); } - }, mUIThreadExecutor).exceptionally(throwable -> { - Log.d(LOGTAG, "Error getting the authentication URL: " + throwable.getLocalizedMessage()); - throwable.printStackTrace(); - return null; - }); + } } @Override @@ -282,12 +294,12 @@ private void updateSyncBindings(boolean isSyncing) { private AccountObserver mAccountListener = new AccountObserver() { @Override - public void onAuthenticated(@NotNull OAuthAccount oAuthAccount, @NotNull AuthType authType) { + public void onAuthenticated(@NonNull OAuthAccount oAuthAccount, @NonNull AuthType authType) { mBinding.setIsSignedIn(true); } @Override - public void onProfileUpdated(@NotNull Profile profile) { + public void onProfileUpdated(@NonNull Profile profile) { } @Override diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/ClippedEventDelegate.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/ClippedEventDelegate.java new file mode 100644 index 000000000..df81d8fcd --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/ClippedEventDelegate.java @@ -0,0 +1,131 @@ +package org.mozilla.vrbrowser.ui.views; + +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Region; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; + +import org.mozilla.vrbrowser.utils.ViewUtils; + +import java.util.Deque; + +public class ClippedEventDelegate implements View.OnHoverListener, View.OnTouchListener { + + private View mView; + private Region mRegion; + private boolean mHovered; + private boolean mTouched; + private OnClickListener mClickListener; + + public ClippedEventDelegate(@DrawableRes int res, @NonNull View view) { + mView = view; + mHovered = false; + mTouched = false; + + view.getViewTreeObserver().addOnGlobalLayoutListener( + () -> { + Path path = createPathFromResource(res); + RectF bounds = new RectF(); + path.computeBounds(bounds, true); + + bounds = new RectF(); + path.computeBounds(bounds, true); + mRegion = new Region(); + mRegion.setPath(path, new Region((int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom)); + }); + } + + public void setOnClickListener(OnClickListener listener) { + mClickListener = listener; + } + + private Path createPathFromResource(@DrawableRes int res) { + VectorShape shape = new VectorShape(mView.getContext(), res); + shape.onResize(mView.getWidth(), mView.getHeight()); + Deque layers = shape.getLayers(); + VectorShape.Layer layer = layers.getFirst(); + + // TODO Handle state changes and update the Region based on the new current state shape + + return layer.transformedPath; + } + + @Override + public boolean onHover(View v, MotionEvent event) { + if(!mRegion.contains((int)event.getX(),(int) event.getY())) { + if (mHovered) { + mHovered = false; + event.setAction(MotionEvent.ACTION_HOVER_EXIT); + return v.onHoverEvent(event); + } + + return true; + + } else { + if (!mHovered) { + mHovered = true; + event.setAction(MotionEvent.ACTION_HOVER_ENTER); + } + + return v.onHoverEvent(event); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + v.getParent().requestDisallowInterceptTouchEvent(true); + + if(!mRegion.contains((int)event.getX(),(int) event.getY())) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + if (mTouched) { + v.requestFocus(); + v.requestFocusFromTouch(); + if (mClickListener != null) { + mClickListener.onClick(v); + } + } + v.setPressed(false); + mTouched = false; + } + + return true; + + } else { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + v.setPressed(true); + mTouched = true; + return true; + + case MotionEvent.ACTION_UP: + if (mTouched && ViewUtils.isInsideView(v, (int)event.getRawX(), (int)event.getRawY())) { + v.requestFocus(); + v.requestFocusFromTouch(); + if (mClickListener != null) { + mClickListener.onClick(v); + } + } + v.setPressed(false); + mTouched = false; + return true; + + case MotionEvent.ACTION_MOVE: + return true; + + case MotionEvent.ACTION_CANCEL: + v.setPressed(false); + mTouched = false; + return true; + } + + return false; + } + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomFastScroller.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomFastScroller.java index 13b726c58..55637623d 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomFastScroller.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomFastScroller.java @@ -498,18 +498,23 @@ private void horizontalScrollTo(float x) { private int scrollTo(float oldDragPos, float newDragPos, int[] scrollbarRange, int scrollRange, int scrollOffset, int viewLength) { - int scrollbarLength = scrollbarRange[1] - scrollbarRange[0]; + final int scrollbarLength = scrollbarRange[1] - scrollbarRange[0]; if (scrollbarLength == 0) { return 0; } - float percentage = ((newDragPos - oldDragPos) / (float) scrollbarLength); - int totalPossibleOffset = scrollRange - viewLength; - int scrollingBy = (int) (percentage * totalPossibleOffset); - int absoluteOffset = scrollOffset + scrollingBy; + final float percentage = ((newDragPos - oldDragPos) / (float)scrollbarLength); + final int totalPossibleOffset = scrollRange - viewLength; + final float scrollingBy = percentage * scrollRange; + final float absoluteOffset = scrollOffset + scrollingBy; + if (absoluteOffset < totalPossibleOffset && absoluteOffset >= 0) { - return scrollingBy; + // Java is down-casting and scrollingBy is a float. Therefore, we need to use ceil() or floor() + // to find out the nearest integer of pixels. + return scrollingBy > 0.0f ? (int) Math.ceil(scrollingBy) : (int) Math.floor(scrollingBy); } else { - return 0; + // When scrolling the last part and its absoluteOffset is out of range of scrollbarLength, + // we still can scroll by its remaining scrollOffset. + return absoluteOffset < 0.0f ? -scrollOffset : totalPossibleOffset - scrollOffset; } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomInlineAutocompleteEditText.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomInlineAutocompleteEditText.java index d28ea4882..97bf45d34 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomInlineAutocompleteEditText.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomInlineAutocompleteEditText.java @@ -3,8 +3,8 @@ import android.content.Context; import android.util.AttributeSet; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import mozilla.components.ui.autocomplete.InlineAutocompleteEditText; @@ -14,15 +14,15 @@ interface OnSelectionChangedCallback { void onSelectionChanged(int selectionStart, int selectionEnd); } - public CustomInlineAutocompleteEditText(@NotNull Context ctx, @Nullable AttributeSet attrs, int defStyleAttr) { + public CustomInlineAutocompleteEditText(@NonNull Context ctx, @Nullable AttributeSet attrs, int defStyleAttr) { super(ctx, attrs, defStyleAttr); } - public CustomInlineAutocompleteEditText(@NotNull Context ctx, @Nullable AttributeSet attrs) { + public CustomInlineAutocompleteEditText(@NonNull Context ctx, @Nullable AttributeSet attrs) { super(ctx, attrs); } - public CustomInlineAutocompleteEditText(@NotNull Context ctx) { + public CustomInlineAutocompleteEditText(@NonNull Context ctx) { super(ctx); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomKeyboardView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomKeyboardView.java index 1ca763853..ee5c5879d 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomKeyboardView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/CustomKeyboardView.java @@ -43,9 +43,12 @@ import android.widget.PopupWindow; import android.widget.TextView; +import androidx.annotation.NonNull; + import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.input.CustomKeyboard; +import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; @@ -271,14 +274,45 @@ public interface OnKeyboardActionListener { /** Whether the requirement of a headset to hear passwords if accessibility is enabled is announced. */ private boolean mHeadsetRequiredToHearPasswordsAnnounced; - // Fork private Drawable mFeaturedKeyBackground; private HashSet mFeaturedKeyCodes = new HashSet<>(); private int mSelectedForegroundColor; private int mForegroundColor; - Handler mHandler; + private Handler mHandler; + + private static class MessageHandler extends Handler { + private WeakReference mView; + + + public MessageHandler(@NonNull CustomKeyboardView view) { + mView = new WeakReference<>(view); + } + + @Override + public void handleMessage(Message msg) { + if (mView.get() != null) { + switch (msg.what) { + case MSG_SHOW_PREVIEW: + mView.get().showKey(msg.arg1); + break; + case MSG_REMOVE_PREVIEW: + mView.get().getPreviewText().setVisibility(INVISIBLE); + break; + case MSG_REPEAT: + if (mView.get().repeatKey()) { + Message repeat = Message.obtain(this, MSG_REPEAT); + sendMessageDelayed(repeat, REPEAT_INTERVAL); + } + break; + case MSG_LONGPRESS: + mView.get().openPopupIfRequired((MotionEvent) msg.obj); + break; + } + } + } + } public CustomKeyboardView(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -359,28 +393,7 @@ protected void onAttachedToWindow() { super.onAttachedToWindow(); initGestureDetector(); if (mHandler == null) { - mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_SHOW_PREVIEW: - showKey(msg.arg1); - break; - case MSG_REMOVE_PREVIEW: - mPreviewText.setVisibility(INVISIBLE); - break; - case MSG_REPEAT: - if (repeatKey()) { - Message repeat = Message.obtain(this, MSG_REPEAT); - sendMessageDelayed(repeat, REPEAT_INTERVAL); - } - break; - case MSG_LONGPRESS: - openPopupIfRequired((MotionEvent) msg.obj); - break; - } - } - }; + mHandler = new MessageHandler(this); } } @@ -823,18 +836,18 @@ private void onBufferDraw() { // Draw the text canvas.drawText(label, - (key.width - padding.left - padding.right) / 2 + (key.width - padding.left - padding.right) / 2.0f + padding.left + statePadding, - (key.height - padding.top - padding.bottom) / 2 + (key.height - padding.top - padding.bottom) / 2.0f + (paint.getTextSize() / 2) - descent + padding.top + statePadding, paint); // Turn off drop shadow paint.setShadowLayer(0, 0, 0, 0); } else if (key.icon != null) { - final float drawableX = (key.width - padding.left - padding.right - key.icon.getIntrinsicWidth()) / 2 + final float drawableX = (key.width - padding.left - padding.right - key.icon.getIntrinsicWidth()) / 2.0f + padding.left + statePadding; - final float drawableY = (key.height - padding.top - padding.bottom - key.icon.getIntrinsicHeight()) / 2 + final float drawableY = (key.height - padding.top - padding.bottom - key.icon.getIntrinsicHeight()) / 2.0f + padding.top + statePadding; canvas.translate(drawableX, drawableY); key.icon.setColorFilter(targetColor, PorterDuff.Mode.MULTIPLY); @@ -963,6 +976,10 @@ private void detectAndSendKey(int index, int x, int y, long eventTime) { } } + private TextView getPreviewText() { + return mPreviewText; + } + /** * Handle multi-tap keys by producing the key label for the current multi-tap state. */ @@ -1243,7 +1260,7 @@ public boolean onHoverEvent(MotionEvent event) { } public void setFeaturedKeyBackground(int resId, int[] keyCodes) { - mFeaturedKeyBackground = getResources().getDrawable(resId); + mFeaturedKeyBackground = getResources().getDrawable(resId, getContext().getTheme()); mFeaturedKeyCodes.clear(); for (int value: keyCodes) { mFeaturedKeyCodes.add(value); @@ -1270,8 +1287,7 @@ private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) { mSwipeTracker.addMovement(me); // Ignore all motion events until a DOWN. - if (mAbortKey - && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) { + if (mAbortKey && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) { return true; } @@ -1336,12 +1352,11 @@ private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) { continueLongPress = true; } else if (mRepeatKeyIndex == NOT_A_KEY) { resetMultiTap(); - mLastKey = mCurrentKey; + mLastKey = mDownKey; mLastCodeX = mLastX; mLastCodeY = mLastY; - mLastKeyTime = - mCurrentKeyTime + eventTime - mLastMoveTime; - mCurrentKey = keyIndex; + mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; + mCurrentKey = mDownKey; mCurrentKeyTime = 0; } } @@ -1505,9 +1520,9 @@ private static class SwipeTracker { static final int NUM_PAST = 4; static final int LONGEST_PAST_TIME = 200; - final float mPastX[] = new float[NUM_PAST]; - final float mPastY[] = new float[NUM_PAST]; - final long mPastTime[] = new long[NUM_PAST]; + final float[] mPastX = new float[NUM_PAST]; + final float[] mPastY = new float[NUM_PAST]; + final long[] mPastTime = new long[NUM_PAST]; float mYVelocity; float mXVelocity; @@ -1516,7 +1531,7 @@ public void clear() { mPastTime[0] = 0; } - public void addMovement(MotionEvent ev) { + void addMovement(MotionEvent ev) { long time = ev.getEventTime(); final int N = ev.getHistorySize(); for (int i=0; i mHistoryViewListeners.forEach((listener) -> listener.onHideContextMenu(v))); mBinding.historyList.setHasFixedSize(true); mBinding.historyList.setItemViewCacheSize(20); mBinding.historyList.setDrawingCacheEnabled(true); @@ -157,20 +159,15 @@ public void onClick(View view, VisitInfo item) { Session session = SessionStore.get().getActiveSession(); session.loadUri(item.getUrl()); + + mHistoryViewListeners.forEach((listener) -> listener.onClickItem(view, item)); } @Override public void onDelete(View view, VisitInfo item) { mBinding.historyList.requestFocusFromTouch(); - mHistoryAdapter.removeItem(item); - if (mHistoryAdapter.itemCount() == 0) { - mBinding.setIsEmpty(true); - mBinding.setIsLoading(false); - mBinding.executePendingBindings(); - } - - SessionStore.get().getHistoryStore().deleteHistory(item.getUrl(), item.getVisitTime()); + SessionStore.get().getHistoryStore().deleteVisitsFor(item.getUrl()); } @Override @@ -208,18 +205,33 @@ public void onSyncHistory(@NonNull View view) { @Override public void onFxALogin(@NonNull View view) { - mAccounts.getAuthenticationUrlAsync().thenAcceptAsync((url) -> { - if (url != null) { - mAccounts.setLoginOrigin(Accounts.LoginOrigin.HISTORY); - WidgetManagerDelegate widgetManager = ((VRBrowserActivity)getContext()); - widgetManager.openNewTabForeground(url); - widgetManager.getFocusedWindow().getSession().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_MOBILE); + if (mAccounts.getAccountStatus() == Accounts.AccountStatus.SIGNED_IN) { + mAccounts.logoutAsync(); + + } else { + CompletableFuture result = mAccounts.authUrlAsync(); + if (result != null) { + result.thenAcceptAsync((url) -> { + if (url == null) { + mAccounts.logoutAsync(); + + } else { + mAccounts.setLoginOrigin(Accounts.LoginOrigin.HISTORY); + WidgetManagerDelegate widgetManager = ((VRBrowserActivity) getContext()); + widgetManager.openNewTabForeground(url); + widgetManager.getFocusedWindow().getSession().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_MOBILE); + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.FXA_LOGIN); + + mHistoryViewListeners.forEach((listener) -> listener.onFxALogin(view)); + } + + }, mUIThreadExecutor).exceptionally(throwable -> { + Log.d(LOGTAG, "Error getting the authentication URL: " + throwable.getLocalizedMessage()); + throwable.printStackTrace(); + return null; + }); } - }, mUIThreadExecutor).exceptionally(throwable -> { - Log.d(LOGTAG, "Error getting the authentication URL: " + throwable.getLocalizedMessage()); - throwable.printStackTrace(); - return null; - }); + } } @Override @@ -279,12 +291,12 @@ private void updateSyncBindings(boolean isSyncing) { private AccountObserver mAccountListener = new AccountObserver() { @Override - public void onAuthenticated(@NotNull OAuthAccount oAuthAccount, @NotNull AuthType authType) { + public void onAuthenticated(@NonNull OAuthAccount oAuthAccount, @NonNull AuthType authType) { mBinding.setIsSignedIn(true); } @Override - public void onProfileUpdated(@NotNull Profile profile) { + public void onProfileUpdated(@NonNull Profile profile) { } @Override @@ -298,7 +310,7 @@ public void onAuthenticationProblems() { } }; - @NotNull + @NonNull public static Predicate distinctByUrl(Function keyExtractor) { Set seen = ConcurrentHashMap.newKeySet(); return t -> seen.add(keyExtractor.apply(t)); @@ -362,7 +374,6 @@ private void showHistory(List historyItems) { mBinding.setIsEmpty(false); mBinding.setIsLoading(false); mHistoryAdapter.setHistoryList(historyItems); - mBinding.historyList.post(() -> mBinding.historyList.smoothScrollToPosition(0)); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HoneycombButton.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HoneycombButton.java index ca254f693..86e1a50bf 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HoneycombButton.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HoneycombButton.java @@ -8,7 +8,6 @@ import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; -import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -20,7 +19,6 @@ import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.utils.DeviceType; import org.mozilla.vrbrowser.utils.SystemUtils; -import org.mozilla.vrbrowser.utils.ViewUtils; public class HoneycombButton extends LinearLayout { @@ -33,6 +31,8 @@ public class HoneycombButton extends LinearLayout { private float mButtonTextSize; private String mSecondaryButtonText; private Drawable mButtonIcon; + private boolean mButtonIconHover; + private VectorClippedEventDelegate mEventDelegate; public HoneycombButton(Context context, @Nullable AttributeSet attrs) { this(context, attrs, R.style.honeycombButtonTheme); @@ -44,6 +44,7 @@ public HoneycombButton(Context context, @Nullable AttributeSet attrs, int defSty mButtonText = attributes.getString(R.styleable.HoneycombButton_honeycombButtonText); mButtonTextSize = attributes.getDimension(R.styleable.HoneycombButton_honeycombButtonTextSize, 0.0f); mButtonIcon = attributes.getDrawable(R.styleable.HoneycombButton_honeycombButtonIcon); + mButtonIconHover = attributes.getBoolean(R.styleable.HoneycombButton_honeycombButtonIconHover, true); String iconIdStr = attributes.getString(R.styleable.HoneycombButton_honeycombButtonIcon); int deviceTypeId = DeviceType.getType(); @@ -108,37 +109,53 @@ private void initialize(Context aContext) { mSecondaryText.setClickable(false); } - setOnHoverListener((view, motionEvent) -> false); + mEventDelegate = new VectorClippedEventDelegate(R.drawable.settings_honeycomb_background, this); + setOnHoverListener(mEventDelegate); + setOnTouchListener(mEventDelegate); } - @Override public void setOnClickListener(@Nullable OnClickListener aListener) { - ViewUtils.setStickyClickListener(this, aListener); + mEventDelegate.setOnClickListener(aListener); } @Override - public void setOnHoverListener(final OnHoverListener l) { - super.setOnHoverListener((view, motionEvent) -> { - switch (motionEvent.getAction()) { - case MotionEvent.ACTION_HOVER_ENTER: - if (mIcon != null && mText != null) { + public boolean onHoverEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_ENTER: + if (mIcon != null && mText != null) { + if (mButtonIconHover) { mIcon.setColorFilter(new PorterDuffColorFilter(getResources().getColor(R.color.asphalt, getContext().getTheme()), PorterDuff.Mode.MULTIPLY)); - mText.setTextColor(getContext().getColor(R.color.asphalt)); - mSecondaryText.setTextColor(getContext().getColor(R.color.asphalt)); } - break; - case MotionEvent.ACTION_HOVER_EXIT: - if (mIcon != null && mText != null) { + mText.setTextColor(getContext().getColor(R.color.asphalt)); + mSecondaryText.setTextColor(getContext().getColor(R.color.asphalt)); + } + break; + case MotionEvent.ACTION_HOVER_EXIT: + if (mIcon != null && mText != null) { + if (mButtonIconHover) { mIcon.setColorFilter(new PorterDuffColorFilter(getResources().getColor(R.color.fog, getContext().getTheme()), PorterDuff.Mode.MULTIPLY)); - mText.setTextColor(getContext().getColor(R.color.fog)); - mSecondaryText.setTextColor(getContext().getColor(R.color.fog)); } - break; - } + mText.setTextColor(getContext().getColor(R.color.fog)); + mSecondaryText.setTextColor(getContext().getColor(R.color.fog)); + } + break; + } - return l.onHover(view, motionEvent); - }); + if (mEventDelegate.isInside(event)) { + return super.onHoverEvent(event); + + } else { + setHovered(false); + if (mIcon != null && mText != null) { + if (mButtonIconHover) { + mIcon.setColorFilter(new PorterDuffColorFilter(getResources().getColor(R.color.fog, getContext().getTheme()), PorterDuff.Mode.MULTIPLY)); + } + mText.setTextColor(getContext().getColor(R.color.fog)); + mSecondaryText.setTextColor(getContext().getColor(R.color.fog)); + } + return false; + } } @Override diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/LanguageSelectorView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/KeyboardSelectorView.java similarity index 59% rename from app/src/common/shared/org/mozilla/vrbrowser/ui/views/LanguageSelectorView.java rename to app/src/common/shared/org/mozilla/vrbrowser/ui/views/KeyboardSelectorView.java index 3f942ee78..ba399e545 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/LanguageSelectorView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/KeyboardSelectorView.java @@ -5,6 +5,7 @@ import android.util.TypedValue; import android.widget.FrameLayout; import android.widget.GridLayout; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -15,7 +16,7 @@ import java.util.List; -public class LanguageSelectorView extends FrameLayout { +public class KeyboardSelectorView extends FrameLayout { public static class Item { public final String title; public final Object tag; @@ -27,28 +28,29 @@ public Item(String aTitle, Object aTag) { } public interface Delegate { - void onLanguageClick(Item aItem); + void onItemClick(Item aItem); } private GridLayout mLangRowContainer; private Delegate mDelegate; private List mItems; private List mButtons = new ArrayList<>(); - private int mFirstColItemWidth; - private int mSecondColItemWidth; + private int mDomainColItemWidth; + private int mNarrowColItemWidth; + private int mWideColItemWidth; private static final int kMaxItemsPerColumn = 4; - public LanguageSelectorView(@NonNull Context context) { + public KeyboardSelectorView(@NonNull Context context) { super(context); initialize(); } - public LanguageSelectorView(@NonNull Context context, @Nullable AttributeSet attrs) { + public KeyboardSelectorView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); initialize(); } - public LanguageSelectorView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + public KeyboardSelectorView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initialize(); } @@ -56,8 +58,9 @@ public LanguageSelectorView(@NonNull Context context, @Nullable AttributeSet att private void initialize() { inflate(getContext(), R.layout.language_selection, this); mLangRowContainer = findViewById(R.id.langRowContainer); - mFirstColItemWidth = WidgetPlacement.pixelDimension(getContext(), R.dimen.lang_selector_first_col_item_width); - mSecondColItemWidth = WidgetPlacement.pixelDimension(getContext(), R.dimen.lang_selector_second_col_item_width); + mDomainColItemWidth = WidgetPlacement.pixelDimension(getContext(), R.dimen.lang_selector_domain_col_item_width); + mNarrowColItemWidth = WidgetPlacement.pixelDimension(getContext(), R.dimen.lang_selector_narrow_col_item_width); + mWideColItemWidth = WidgetPlacement.pixelDimension(getContext(), R.dimen.lang_selector_wide_col_item_width); } public void setDelegate(Delegate aDelegate) { @@ -73,14 +76,34 @@ public void setItems(List aItems) { mLangRowContainer.setColumnCount(columns); mLangRowContainer.setRowCount(rows); - int index = 0; - for (Item item: aItems) { + int[] columnWidth = new int[columns]; + for (int i=0; i getItems() { private OnClickListener clickHandler = v -> { UITextButton button = (UITextButton) v; if (mDelegate != null) { - mDelegate.onLanguageClick((Item)button.getTag()); + mDelegate.onItemClick((Item)button.getTag()); } }; - private UITextButton createLangButton(Item aItem) { + private UITextButton createItemButton(Item aItem) { UITextButton button = new UITextButton(getContext()); button.setTintColorList(R.drawable.lang_selector_button_color); button.setBackground(getContext().getDrawable(R.drawable.lang_selector_button_background)); 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 00dea3396..1e6df4210 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 @@ -23,6 +23,7 @@ import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; +import android.widget.EditText; import android.widget.FrameLayout; import androidx.annotation.NonNull; @@ -38,6 +39,7 @@ import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.databinding.NavigationUrlBinding; import org.mozilla.vrbrowser.search.SearchEngineWrapper; +import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.widgets.UIWidget; import org.mozilla.vrbrowser.ui.widgets.dialogs.SelectionActionWidget; @@ -50,7 +52,8 @@ import java.net.URI; import java.net.URL; import java.net.URLDecoder; -import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.concurrent.Executor; import kotlin.Unit; @@ -96,7 +99,7 @@ public interface NavigationURLBarDelegate { void onVoiceSearchClicked(); void onShowAwesomeBar(); void onHideAwesomeBar(); - void onLongPress(float centerX, SelectionActionWidget actionMenu); + void onURLSelectionAction(EditText aURLEdit, float centerX, SelectionActionWidget actionMenu); void onPopUpButtonClicked(); } @@ -205,7 +208,7 @@ private void initialize(Context aContext) { showSelectionMenu(); } else { - mDelegate.onLongPress(getSelectionCenterX(), mSelectionMenu); + mDelegate.onURLSelectionAction(mBinding.urlEditText, getSelectionCenterX(), mSelectionMenu); mSelectionMenu.updateWidget(); } } @@ -238,7 +241,10 @@ private void initialize(Context aContext) { mURLWebsiteColor = typedValue.data; // Bookmarks - mBinding.bookmarkButton.setOnClickListener(v -> handleBookmarkClick()); + mBinding.bookmarkButton.setOnClickListener(v -> { + v.requestFocusFromTouch(); + handleBookmarkClick(); + }); // Initialize bindings mBinding.setIsLibraryVisible(false); @@ -412,6 +418,10 @@ private void setIsBookmarked(boolean aValue) { } public void setPrivateMode(boolean isEnabled) { + mBinding.bookmarkButton.setPrivateMode(isEnabled); + mBinding.microphoneButton.setPrivateMode(isEnabled); + mBinding.clearButton.setPrivateMode(isEnabled); + mBinding.setIsPrivateMode(isEnabled); } @@ -447,6 +457,7 @@ public void handleURLEdit(String text) { if (uri != null) { url = uri.toString(); TelemetryWrapper.urlBarEvent(true); + GleanMetricsService.urlBarEvent(true); } else if (text.startsWith("about:") || text.startsWith("resource://")) { url = text; } else { @@ -454,6 +465,7 @@ public void handleURLEdit(String text) { // Doing search in the URL bar, so sending "aIsURL: false" to telemetry. TelemetryWrapper.urlBarEvent(false); + GleanMetricsService.urlBarEvent(false); } if (mSession.getCurrentUri() != url) { @@ -478,12 +490,14 @@ public void setClickable(boolean clickable) { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } + view.requestFocusFromTouch(); if (mDelegate != null) { mDelegate.onVoiceSearchClicked(); } TelemetryWrapper.voiceInputEvent(); + GleanMetricsService.voiceInputEvent(); }; private OnClickListener mClearListener = view -> { @@ -499,6 +513,7 @@ public void setClickable(boolean clickable) { mAudio.playSound(AudioEngine.Sound.CLICK); } + view.requestFocusFromTouch(); if (mDelegate != null) { mDelegate.onPopUpButtonClicked(); } @@ -555,7 +570,7 @@ public boolean onDoubleTapEvent(MotionEvent motionEvent) { }; private void showSelectionMenu() { - ArrayList actions = new ArrayList<>(); + Collection actions = new HashSet<>(); if (mBinding.urlEditText.getSelectionEnd() > mBinding.urlEditText.getSelectionStart()) { actions.add(GeckoSession.SelectionActionDelegate.ACTION_CUT); actions.add(GeckoSession.SelectionActionDelegate.ACTION_COPY); @@ -574,15 +589,14 @@ private void showSelectionMenu() { return; } - String[] actionsArray = actions.toArray(new String[0]); - if (mSelectionMenu != null && !mSelectionMenu.hasSameActions(actionsArray)) { + if (mSelectionMenu != null && !mSelectionMenu.hasSameActions(actions)) { // Release current selection menu to recreate it with different actions. hideSelectionMenu(); } if (mSelectionMenu == null) { mSelectionMenu = new SelectionActionWidget(getContext()); - mSelectionMenu.setActions(actionsArray); + mSelectionMenu.setActions(actions); mSelectionMenu.setDelegate(new SelectionActionWidget.Delegate() { @Override public void onAction(String action) { @@ -597,10 +611,12 @@ public void onAction(String action) { } else if (action.equals(GeckoSession.SelectionActionDelegate.ACTION_COPY) && selectionValid) { String selectedText = mBinding.urlEditText.getText().toString().substring(startSelection, endSelection); clipboard.setPrimaryClip(ClipData.newPlainText("text", selectedText)); + mBinding.urlEditText.setSelection(endSelection); } else if (action.equals(GeckoSession.SelectionActionDelegate.ACTION_PASTE) && clipboard.hasPrimaryClip()) { ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); if (selectionValid) { mBinding.urlEditText.setText(StringUtils.removeRange(mBinding.urlEditText.getText().toString(), startSelection, endSelection)); + mBinding.urlEditText.setSelection(startSelection); } if (item != null && item.getText() != null) { mBinding.urlEditText.getText().insert(mBinding.urlEditText.getSelectionStart(), item.getText()); @@ -624,7 +640,7 @@ public void onDismiss() { } if (mDelegate != null) { - mDelegate.onLongPress(getSelectionCenterX(), mSelectionMenu); + mDelegate.onURLSelectionAction(mBinding.urlEditText, getSelectionCenterX(), mSelectionMenu); } mSelectionMenu.show(UIWidget.KEEP_FOCUS); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VectorClippedEventDelegate.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VectorClippedEventDelegate.java new file mode 100644 index 000000000..dcf8777c1 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VectorClippedEventDelegate.java @@ -0,0 +1,144 @@ +package org.mozilla.vrbrowser.ui.views; + +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Region; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewTreeObserver; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; + +import org.mozilla.vrbrowser.utils.ViewUtils; + +import java.util.Deque; + +public class VectorClippedEventDelegate implements View.OnHoverListener, View.OnTouchListener { + + private View mView; + private @DrawableRes int mRes; + private Region mRegion; + private boolean mHovered; + private boolean mTouched; + private OnClickListener mClickListener; + + VectorClippedEventDelegate(@DrawableRes int res, @NonNull View view) { + mView = view; + mRes = res; + mHovered = false; + mTouched = false; + + view.getViewTreeObserver().addOnGlobalLayoutListener(mLayoutListener); + } + + private ViewTreeObserver.OnGlobalLayoutListener mLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + Path path = createPathFromResource(mRes); + RectF bounds = new RectF(); + path.computeBounds(bounds, true); + + bounds = new RectF(); + path.computeBounds(bounds, true); + mRegion = new Region(); + mRegion.setPath(path, new Region((int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom)); + + mView.getViewTreeObserver().removeOnGlobalLayoutListener(mLayoutListener); + } + }; + + public void setOnClickListener(OnClickListener listener) { + mClickListener = listener; + } + + private Path createPathFromResource(@DrawableRes int res) { + VectorShape shape = new VectorShape(mView.getContext(), res); + shape.onResize(mView.getWidth(), mView.getHeight()); + Deque layers = shape.getLayers(); + VectorShape.Layer layer = layers.getFirst(); + + // TODO Handle state changes and update the Region based on the new current state shape + + return layer.transformedPath; + } + + @Override + public boolean onHover(View v, MotionEvent event) { + if(!isInside(event)) { + if (mHovered) { + mHovered = false; + event.setAction(MotionEvent.ACTION_HOVER_EXIT); + return v.onHoverEvent(event); + } + + return false; + + } else { + if (!mHovered) { + mHovered = true; + event.setAction(MotionEvent.ACTION_HOVER_ENTER); + } + + return v.onHoverEvent(event); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + v.getParent().requestDisallowInterceptTouchEvent(true); + + if(!isInside(event)) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + if (mTouched) { + v.requestFocus(); + v.requestFocusFromTouch(); + if (mClickListener != null) { + mClickListener.onClick(v); + } + } + v.setPressed(false); + mTouched = false; + } + + return true; + + } else { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + v.setPressed(true); + mTouched = true; + return true; + + case MotionEvent.ACTION_UP: + if (mTouched && ViewUtils.isInsideView(v, (int)event.getRawX(), (int)event.getRawY())) { + v.requestFocus(); + v.requestFocusFromTouch(); + if (mClickListener != null) { + mClickListener.onClick(v); + } + } + v.setPressed(false); + mTouched = false; + return true; + + case MotionEvent.ACTION_MOVE: + return true; + + case MotionEvent.ACTION_CANCEL: + v.setPressed(false); + mTouched = false; + return true; + } + + return false; + } + } + + public boolean isInside(MotionEvent event) { + return mRegion.contains((int)event.getX(),(int) event.getY()); + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VectorShape.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VectorShape.java new file mode 100644 index 000000000..93a950af5 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VectorShape.java @@ -0,0 +1,111 @@ +package org.mozilla.vrbrowser.ui.views; + +import android.content.Context; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.drawable.shapes.Shape; +import android.util.AttributeSet; +import android.util.Xml; + +import androidx.core.graphics.PathParser; + +import org.jetbrains.annotations.NotNull; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; + +class VectorShape extends Shape { + private static final String TAG_VECTOR = "vector"; + private static final String TAG_PATH = "path"; + + private RectF viewportRect = new RectF(); + private List layers = new ArrayList<>(); + + VectorShape(Context context, int id) { + XmlResourceParser parser = context.getResources().getXml(id); + AttributeSet set = Xml.asAttributeSet(parser); + try { + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + if(eventType == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + if (tagName.equals(TAG_VECTOR)) { + int[] attrs = { android.R.attr.viewportWidth, android.R.attr.viewportHeight }; + TypedArray ta = context.obtainStyledAttributes(set, attrs); + viewportRect.set(0, 0, ta.getFloat(0, 0), ta.getFloat(1, 0)); + ta.recycle(); + + } else if (tagName.equals(TAG_PATH)) { + int[] attrs = { android.R.attr.name, android.R.attr.fillColor, android.R.attr.pathData }; + TypedArray ta = context.obtainStyledAttributes(set, attrs); + layers.add(new Layer(ta.getString(2), ta.getColor(1, 0xdeadc0de), ta.getString(0))); + ta.recycle(); + } + } + eventType = parser.next(); + } + + } catch (XmlPullParserException | IOException e) { + e.printStackTrace(); + } + } + + public Deque getLayers() { + Deque outLayers = new LinkedList<>(); + for (Layer layer : layers) { + outLayers.addLast(layer); + } + + return outLayers; + } + + @Override + protected void onResize(float width, float height) { + Matrix matrix = new Matrix(); + Region shapeRegion = new Region(0, 0, (int) width, (int) height); + matrix.setRectToRect(viewportRect, new RectF(0, 0, width, height), Matrix.ScaleToFit.CENTER); + for (Layer layer : layers) { + layer.transform(matrix, shapeRegion); + } + } + + @Override + public void draw(Canvas canvas, Paint paint) { + for (Layer layer : layers) { + canvas.drawPath(layer.transformedPath, layer.paint); + } + } + + class Layer { + Path originalPath; + Path transformedPath = new Path(); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + Region region = new Region(); + String name; + + public Layer(String data, int color, String name) { + originalPath = PathParser.createPathFromPathData(data); + paint.setColor(color); + this.name = name; + } + + public void transform(Matrix matrix, Region clip) { + originalPath.transform(matrix, transformedPath); + region.setPath(transformedPath, clip); + } + + @NotNull + @Override public String toString() { return name; } + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/settings/RadioGroupSetting.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/settings/RadioGroupSetting.java index 7f4d65be8..8bfc2c7a4 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/settings/RadioGroupSetting.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/settings/RadioGroupSetting.java @@ -103,7 +103,6 @@ public void onCheckedChanged(RadioGroup compoundButton, @IdRes int checkedId) { } setChecked(checkedId, true); - compoundButton.requestFocus(); } }; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/settings/SingleEditSetting.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/settings/SingleEditSetting.java index a76a50768..d9334d3ab 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/settings/SingleEditSetting.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/settings/SingleEditSetting.java @@ -11,12 +11,12 @@ import android.widget.LinearLayout; import android.widget.TextView; -import org.jetbrains.annotations.NotNull; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; -import androidx.annotation.StringRes; - public class SingleEditSetting extends LinearLayout { private AudioEngine mAudio; @@ -162,7 +162,7 @@ public boolean isEditing() { return mEdit1.getVisibility() == View.VISIBLE; } - public boolean contains(@NotNull View view) { + public boolean contains(@NonNull View view) { return findViewById(view.getId()) == view; } 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 223a5f3ce..55fd77831 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 @@ -36,6 +36,7 @@ import org.mozilla.vrbrowser.browser.engine.Session; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.input.CustomKeyboard; +import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.keyboards.DanishKeyboard; import org.mozilla.vrbrowser.ui.keyboards.FinnishKeyboard; @@ -54,7 +55,7 @@ import org.mozilla.vrbrowser.ui.keyboards.SwedishKeyboard; import org.mozilla.vrbrowser.ui.views.AutoCompletionView; import org.mozilla.vrbrowser.ui.views.CustomKeyboardView; -import org.mozilla.vrbrowser.ui.views.LanguageSelectorView; +import org.mozilla.vrbrowser.ui.views.KeyboardSelectorView; import org.mozilla.vrbrowser.ui.widgets.dialogs.VoiceSearchWidget; import org.mozilla.vrbrowser.ui.keyboards.ChinesePinyinKeyboard; import org.mozilla.vrbrowser.ui.keyboards.EnglishKeyboard; @@ -91,7 +92,8 @@ public class KeyboardWidget extends UIWidget implements CustomKeyboardView.OnKey private EditorInfo mEditorInfo = new EditorInfo(); private VoiceSearchWidget mVoiceSearchWidget; private AutoCompletionView mAutoCompletionView; - private LanguageSelectorView mLanguageSelectorView; + private KeyboardSelectorView mLanguageSelectorView; + private KeyboardSelectorView mDomainSelectorView; private int mKeyWidth; private int mKeyboardPopupTopMargin; @@ -178,19 +180,22 @@ private void initialize(Context aContext) { mAutoCompletionView.setExtendedHeight((int)(mWidgetPlacement.height * mWidgetPlacement.density)); mAutoCompletionView.setDelegate(this); + mDomainSelectorView = findViewById(R.id.domainSelectorView); + mDomainSelectorView.setDelegate(this::handleDomainChange); + mKeyboards = new ArrayList<>(); mKeyboards.add(new EnglishKeyboard(aContext)); - mKeyboards.add(new ItalianKeyboard(aContext)); + mKeyboards.add(new ChinesePinyinKeyboard(aContext)); + mKeyboards.add(new ChineseZhuyinKeyboard(aContext)); + mKeyboards.add(new JapaneseKeyboard(aContext)); mKeyboards.add(new FrenchKeyboard(aContext)); mKeyboards.add(new GermanKeyboard(aContext)); mKeyboards.add(new SpanishKeyboard(aContext)); mKeyboards.add(new RussianKeyboard(aContext)); - mKeyboards.add(new ChinesePinyinKeyboard(aContext)); - mKeyboards.add(new ChineseZhuyinKeyboard(aContext)); mKeyboards.add(new KoreanKeyboard(aContext)); - mKeyboards.add(new JapaneseKeyboard(aContext)); - mKeyboards.add(new PolishKeyboard(aContext)); + mKeyboards.add(new ItalianKeyboard(aContext)); mKeyboards.add(new DanishKeyboard(aContext)); + mKeyboards.add(new PolishKeyboard(aContext)); mKeyboards.add(new NorwegianKeyboard(aContext)); mKeyboards.add(new SwedishKeyboard(aContext)); mKeyboards.add(new FinnishKeyboard(aContext)); @@ -406,6 +411,7 @@ private void hideOverlays() { mPopupKeyboardView.setVisibility(View.GONE); mPopupKeyboardLayer.setVisibility(View.GONE); mLanguageSelectorView.setVisibility(View.GONE); + mDomainSelectorView.setVisibility(View.GONE); } protected void onDismiss() { @@ -458,8 +464,6 @@ public void onLongPress(Keyboard.Key popupKey) { } else if (popupKey.codes[0] == CustomKeyboard.KEYCODE_SHIFT) { mIsLongPress = !mIsCapsLock; - } else if (popupKey.codes[0] == Keyboard.KEYCODE_DELETE) { - handleBackspace(true); } } @@ -484,7 +488,7 @@ public void onKey(int primaryCode, int[] keyCodes, boolean hasPopup) { handleShift(!mKeyboardView.isShifted()); break; case Keyboard.KEYCODE_DELETE: - handleBackspace(false); + handleBackspace(); break; case Keyboard.KEYCODE_DONE: handleDone(); @@ -498,6 +502,9 @@ public void onKey(int primaryCode, int[] keyCodes, boolean hasPopup) { case CustomKeyboard.KEYCODE_EMOJI: handleEmojiInput(); break; + case CustomKeyboard.KEYCODE_DOMAIN: + handleDomain(); + break; case ' ': handleSpace(); break; @@ -637,7 +644,7 @@ private void handleShift(boolean isShifted) { mKeyboardView.setShifted(shifted || mIsCapsLock); } - private void handleBackspace(final boolean isLongPress) { + private void handleBackspace() { final InputConnection connection = mInputConnection; if (mComposingText.length() > 0) { CharSequence selectedText = mInputConnection.getSelectedText(0); @@ -660,33 +667,26 @@ private void handleBackspace(final boolean isLongPress) { return; } - if (isLongPress) { - CharSequence currentText = connection.getExtractedText(new ExtractedTextRequest(), 0).text; - CharSequence beforeCursorText = connection.getTextBeforeCursor(currentText.length(), 0); - CharSequence afterCursorText = connection.getTextAfterCursor(currentText.length(), 0); - connection.deleteSurroundingText(beforeCursorText.length(), afterCursorText.length()); - } else { - if (mCurrentKeyboard.usesTextOverride()) { - String beforeText = getTextBeforeCursor(connection); - String newBeforeText = mCurrentKeyboard.overrideBackspace(beforeText); - if (newBeforeText != null) { - // Replace whole before text - connection.deleteSurroundingText(beforeText.length(), 0); - connection.commitText(newBeforeText, 1); - return; - } + if (mCurrentKeyboard.usesTextOverride()) { + String beforeText = getTextBeforeCursor(connection); + String newBeforeText = mCurrentKeyboard.overrideBackspace(beforeText); + if (newBeforeText != null) { + // Replace whole before text + connection.deleteSurroundingText(beforeText.length(), 0); + connection.commitText(newBeforeText, 1); + return; } - // Remove the character before the cursor. - connection.deleteSurroundingText(1, 0); } + // Remove the character before the cursor. + connection.deleteSurroundingText(1, 0); }); } private void handleGlobeClick() { if (mLanguageSelectorView.getItems() == null || mLanguageSelectorView.getItems().size() == 0) { - ArrayList items = new ArrayList<>(); + ArrayList items = new ArrayList<>(); for (KeyboardInterface keyboard: mKeyboards) { - items.add(new LanguageSelectorView.Item(keyboard.getKeyboardTitle(), keyboard)); + items.add(new KeyboardSelectorView.Item(StringUtils.capitalize(keyboard.getKeyboardTitle()), keyboard)); } mLanguageSelectorView.setItems(items); } @@ -701,6 +701,17 @@ private void handleEmojiInput() { mAutoCompletionView.setItems(candidates != null ? candidates.words : null); } + private void handleDomain() { + ArrayList items = new ArrayList<>(); + for (String item: mCurrentKeyboard.getDomains()) { + items.add(new KeyboardSelectorView.Item(item, item)); + } + mDomainSelectorView.setItems(items); + + mDomainSelectorView.setVisibility(View.VISIBLE); + mPopupKeyboardLayer.setVisibility(View.VISIBLE); + } + private void handleLanguageChange(KeyboardInterface aKeyboard) { cleanComposingText(); @@ -733,6 +744,15 @@ private void handleLanguageChange(KeyboardInterface aKeyboard) { mCurrentKeyboard.getAlphabeticKeyboard().setSpaceKeyLabel(spaceText); } + private void handleDomainChange(KeyboardSelectorView.Item aItem) { + handleText(aItem.title); + + disableShift(getSymbolsKeyboard()); + handleShift(false); + hideOverlays(); + updateCandidates(); + } + private void disableShift(@NonNull CustomKeyboard keyboard) { int[] shiftIndices = keyboard.getShiftKeyIndices(); for (int shiftIndex: shiftIndices) { @@ -847,6 +867,7 @@ private void handleVoiceInput() { } mIsInVoiceInput = true; TelemetryWrapper.voiceInputEvent(); + GleanMetricsService.voiceInputEvent(); mVoiceSearchWidget.show(CLEAR_FOCUS); mWidgetPlacement.visible = false; mWidgetManager.updateWidget(this); @@ -1002,7 +1023,7 @@ public boolean dispatchKeyEvent(final KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DEL: - handleBackspace(event.isLongPress()); + handleBackspace(); return true; case KeyEvent.KEYCODE_ENTER: case KeyEvent.KEYCODE_NUMPAD_ENTER: 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 9bb25ae00..42a881d1e 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 @@ -85,11 +85,13 @@ private void initialize(Context aContext) { } else { mMedia.play(); } + + mMediaPlayButton.requestFocusFromTouch(); }); mMediaSeekBackButton.setOnClickListener(v -> { mMedia.seek(Math.max(0, mMedia.getCurrentTime() - 10.0f)); - + mMediaSeekBackButton.requestFocusFromTouch(); }); mMediaSeekForwardButton.setOnClickListener(v -> { @@ -98,6 +100,7 @@ private void initialize(Context aContext) { t = Math.min(mMedia.getDuration(), t); } mMedia.seek(t); + mMediaSeekForwardButton.requestFocusFromTouch(); }); mMediaProjectionButton.setOnClickListener(v -> { @@ -111,7 +114,12 @@ private void initialize(Context aContext) { placement.rotationAxisY = 1.0f; placement.rotation = (float) Math.toRadians(-7); } - mProjectionMenu.getPlacement().visible = !mProjectionMenu.getPlacement().visible; + if (mProjectionMenu.isVisible()) { + mProjectionMenu.hide(KEEP_WIDGET); + + } else { + mProjectionMenu.show(REQUEST_FOCUS); + } mWidgetManager.updateWidget(mProjectionMenu); }); @@ -122,13 +130,14 @@ private void initialize(Context aContext) { mMedia.setMuted(true); mVolumeControl.setVolume(0); } + mMediaVolumeButton.requestFocusFromTouch(); }); mMediaBackButton.setOnClickListener(v -> { if (mBackHandler != null) { mBackHandler.run(); } - + mMediaBackButton.requestFocusFromTouch(); }); mSeekBar.setDelegate(new MediaSeekBar.Delegate() { @@ -137,6 +146,7 @@ public void onSeekDragStart() { mPlayOnSeekEnd = mMedia.isPlaying(); mMediaSeekLabel.setVisibility(View.VISIBLE); mMedia.pause(); + mSeekBar.requestFocusFromTouch(); } @Override @@ -180,6 +190,7 @@ public void onSeekPreview(String aText, double aRatio) { if (mMedia.isMuted()) { mMedia.setMuted(false); } + mVolumeControl.requestFocusFromTouch(); }); this.setOnHoverListener((v, event) -> { @@ -267,10 +278,7 @@ public void setMedia(Media aMedia) { onTimeChange(mMedia.getMediaElement(), mMedia.getCurrentTime()); onVolumeChange(mMedia.getMediaElement(), mMedia.getVolume(), mMedia.isMuted()); onReadyStateChange(mMedia.getMediaElement(), mMedia.getReadyState()); - if (mMedia.isPlaying()) { - onPlaybackStateChange(mMedia.getMediaElement(), MediaElement.MEDIA_STATE_PLAY); - } - + onPlaybackStateChange(mMedia.getMediaElement(), mMedia.isPlaying() ? MediaElement.MEDIA_STATE_PLAY : MediaElement.MEDIA_STATE_PAUSE); mMedia.addMediaListener(this); } 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 0c8e79d86..45173425c 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 @@ -16,6 +16,7 @@ import android.util.Pair; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -53,6 +54,8 @@ import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; +import static org.mozilla.vrbrowser.ui.widgets.menus.VideoProjectionMenuWidget.VIDEO_PROJECTION_NONE; + public class NavigationBarWidget extends UIWidget implements GeckoSession.NavigationDelegate, GeckoSession.ProgressDelegate, GeckoSession.ContentDelegate, WidgetManagerDelegate.WorldClickListener, WidgetManagerDelegate.UpdateListener, SessionChangeListener, @@ -101,7 +104,7 @@ public class NavigationBarWidget extends UIWidget implements GeckoSession.Naviga private BrightnessMenuWidget mBrightnessWidget; private MediaControlsWidget mMediaControlsWidget; private Media mFullScreenMedia; - private @VideoProjectionMenuWidget.VideoProjectionFlags Integer mAutoSelectedProjection; + private @VideoProjectionMenuWidget.VideoProjectionFlags int mAutoSelectedProjection = VIDEO_PROJECTION_NONE; private HamburgerMenuWidget mHamburgerMenu; private SendTabDialogWidget mSendTabDialog; private TooltipWidget mPopUpNotification; @@ -251,7 +254,7 @@ private void initialize(@NonNull Context aContext) { mAudio.playSound(AudioEngine.Sound.CLICK); } - if (mAutoSelectedProjection != null) { + if (mAutoSelectedProjection != VIDEO_PROJECTION_NONE) { enterVRVideo(mAutoSelectedProjection); return; } @@ -259,7 +262,7 @@ private void initialize(@NonNull Context aContext) { closeFloatingMenus(); if (!wasVisible) { - mProjectionMenu.setVisible(true); + mProjectionMenu.show(REQUEST_FOCUS); } }); @@ -330,18 +333,12 @@ private void initialize(@NonNull Context aContext) { mWidgetManager.addConnectivityListener(mConnectivityDelegate); mVoiceSearchWidget = createChild(VoiceSearchWidget.class, false); - mVoiceSearchWidget.getPlacement().parentAnchorY = 0.0f; - mVoiceSearchWidget.getPlacement().translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.base_app_dialog_y_distance); mVoiceSearchWidget.setDelegate(this); mSuggestionsProvider = new SuggestionsProvider(getContext()); mPrefs = PreferenceManager.getDefaultSharedPreferences(mAppContext); mPrefs.registerOnSharedPreferenceChangeListener(this); - - for (CustomUIButton button : mButtons) { - button.setPrivateMode(true); - } } @Override @@ -417,7 +414,7 @@ public void detachFromWindow() { } mAttachedWindow = null; if (mAwesomeBar != null && mAwesomeBar.isVisible()) { - onHideAwesomeBar(); + mAwesomeBar.hideNoAnim(UIWidget.KEEP_WIDGET); } } @@ -609,6 +606,8 @@ private void enterResizeMode() { } } + hideNotifications(); + // Update preset styles } @@ -679,6 +678,7 @@ private void enterVRVideo(@VideoProjectionMenuWidget.VideoProjectionFlags int aP mMediaControlsWidget.getPlacement().visible = false; mWidgetManager.addWidget(mMediaControlsWidget); mMediaControlsWidget.setBackHandler(mVRVideoBackHandler); + mMediaControlsWidget.setOnClickListener(v -> v.requestFocusFromTouch()); } mMediaControlsWidget.setProjectionMenuWidget(mProjectionMenu); mMediaControlsWidget.setMedia(mFullScreenMedia); @@ -701,6 +701,7 @@ private void exitVRVideo() { boolean composited = mProjectionMenu.getPlacement().composited; mProjectionMenu.getPlacement().copyFrom(mProjectionMenuPlacement); mProjectionMenu.getPlacement().composited = composited; + mProjectionMenu.setSelectedProjection(VIDEO_PROJECTION_NONE); mWidgetManager.updateWidget(mProjectionMenu); closeFloatingMenus(); mWidgetManager.setControllersVisible(true); @@ -709,6 +710,9 @@ private void exitVRVideo() { mAttachedWindow.disableVRVideoMode(); mAttachedWindow.setVisible(true); mMediaControlsWidget.setVisible(false); + + // Reposition UI in front of the user when exiting a VR video. + mWidgetManager.resetUIYaw(); } private void setResizePreset(float aMultiplier) { @@ -746,10 +750,10 @@ public void updateServoButton() { private void closeFloatingMenus() { if (mProjectionMenu != null) { - mProjectionMenu.setVisible(false); + mProjectionMenu.hide(KEEP_WIDGET); } if (mBrightnessWidget != null) { - mBrightnessWidget.setVisible(false); + mBrightnessWidget.hide(KEEP_WIDGET); } } @@ -778,10 +782,10 @@ public void onLocationChange(GeckoSession session, String url) { @Override public void onCanGoBack(GeckoSession aSession, boolean canGoBack) { if (mBackButton != null) { - boolean enableBackButton = getSession().canGoBack(); - - Log.d(LOGTAG, "Got onCanGoBack: " + (enableBackButton ? "true" : "false")); - mBackButton.setEnabled(enableBackButton); + Log.d(LOGTAG, "Got onCanGoBack: " + (canGoBack ? "true" : "false")); + mBackButton.setEnabled(canGoBack); + mBackButton.setHovered(false); + mBackButton.setClickable(canGoBack); } } @@ -790,6 +794,8 @@ public void onCanGoForward(GeckoSession aSession, boolean canGoForward) { if (mForwardButton != null) { Log.d(LOGTAG, "Got onCanGoForward: " + (canGoForward ? "true" : "false")); mForwardButton.setEnabled(canGoForward); + mForwardButton.setHovered(false); + mForwardButton.setClickable(canGoForward); } } @@ -868,11 +874,14 @@ public void onFullScreen(GeckoSession session, boolean aFullScreen) { } AtomicBoolean autoEnter = new AtomicBoolean(false); mAutoSelectedProjection = VideoProjectionMenuWidget.getAutomaticProjection(getSession().getCurrentUri(), autoEnter); - if (mAutoSelectedProjection != null && autoEnter.get()) { + if (mAutoSelectedProjection != VIDEO_PROJECTION_NONE && autoEnter.get()) { mAutoEnteredVRVideo = true; postDelayed(() -> enterVRVideo(mAutoSelectedProjection), 300); } else { mAutoEnteredVRVideo = false; + if (mProjectionMenu != null) { + mProjectionMenu.setSelectedProjection(mAutoSelectedProjection); + } } } else { if (mIsInVRVideo) { @@ -978,15 +987,15 @@ public void onHideAwesomeBar() { } @Override - public void onLongPress(float centerX, SelectionActionWidget actionMenu) { + public void onURLSelectionAction(EditText aURLEdit, float centerX, SelectionActionWidget actionMenu) { actionMenu.getPlacement().parentHandle = this.getHandle(); actionMenu.getPlacement().parentAnchorY = 1.0f; - actionMenu.getPlacement().anchorY = 0.34f; + actionMenu.getPlacement().anchorY = 0.44f; Rect offsetViewBounds = new Rect(); - mURLBar.getDrawingRect(offsetViewBounds); - offsetDescendantRectToMyCoords(mURLBar, offsetViewBounds); - float x = offsetViewBounds.left + centerX; - actionMenu.getPlacement().parentAnchorX = x / getMeasuredWidth(); + aURLEdit.getDrawingRect(offsetViewBounds); + offsetDescendantRectToMyCoords(aURLEdit, offsetViewBounds); + float x = aURLEdit.getPaddingLeft() + offsetViewBounds.left + centerX; + actionMenu.getPlacement().parentAnchorX = x / getWidth(); } @Override @@ -1051,6 +1060,8 @@ public void onBookmarksShown(WindowWidget aWindow) { mURLBar.setHint(R.string.url_bookmarks_title); mURLBar.setIsLibraryVisible(true); } + + hideNotifications(); } @Override @@ -1071,6 +1082,8 @@ public void onHistoryViewShown(WindowWidget aWindow) { mURLBar.setHint(R.string.url_history_title); mURLBar.setIsLibraryVisible(true); } + + hideNotifications(); } @Override @@ -1180,14 +1193,11 @@ private void hideMenu() { } public void showSendTabDialog() { - mSendTabDialog = new SendTabDialogWidget(getContext()); + if (mSendTabDialog == null) { + mSendTabDialog = new SendTabDialogWidget(getContext()); + } mSendTabDialog.mWidgetPlacement.parentHandle = mWidgetManager.getFocusedWindow().getHandle(); mSendTabDialog.setSessionId(mAttachedWindow.getSession().getId()); - mSendTabDialog.setDelegate(() -> { - mSendTabDialog.releaseWidget(); - mSendTabDialog = null; - NavigationBarWidget.this.show(REQUEST_FOCUS); - }); mSendTabDialog.show(UIWidget.REQUEST_FOCUS); } @@ -1226,6 +1236,10 @@ public void hidePopUpsBlockedNotification() { }); } + public void hideNotifications() { + hidePopUpsBlockedNotification(); + } + private void showNotification(UIButton button, int stringRes) { if (mPopUpNotification != null && mPopUpNotification.isVisible()) { return; @@ -1235,14 +1249,14 @@ private void showNotification(UIButton button, int stringRes) { getDrawingRect(offsetViewBounds); offsetDescendantRectToMyCoords(button, offsetViewBounds); - float ratio = WidgetPlacement.viewToWidgetRatio(getContext(), NavigationBarWidget.this); + float ratio = WidgetPlacement.viewToWidgetRatio(getContext(), this); mPopUpNotification = new TooltipWidget(getContext(), R.layout.library_notification); mPopUpNotification.getPlacement().parentHandle = getHandle(); mPopUpNotification.getPlacement().anchorY = 0.0f; - mPopUpNotification.getPlacement().translationX = (offsetViewBounds.left + button.getWidth() / 2.0f) * ratio; + mPopUpNotification.getPlacement().translationX = (getPaddingLeft() + offsetViewBounds.left + button.getWidth() / 2.0f) * ratio; mPopUpNotification.getPlacement().translationY = ((offsetViewBounds.top - 60) * ratio); - mPopUpNotification.getPlacement().translationZ = 25.0f; + mPopUpNotification.getPlacement().translationZ = 1.0f; mPopUpNotification.getPlacement().density = WidgetPlacement.floatDimension(getContext(), R.dimen.tooltip_default_density); mPopUpNotification.setText(stringRes); mPopUpNotification.setCurvedMode(true); 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 deleted file mode 100644 index 9e65d9a4e..000000000 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NoInternetWidget.java +++ /dev/null @@ -1,70 +0,0 @@ -/* -*- 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; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.Button; - -import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.ui.widgets.dialogs.UIDialog; - -public class NoInternetWidget extends UIDialog { - - public NoInternetWidget(Context aContext) { - super(aContext); - initialize(aContext); - } - - public NoInternetWidget(Context aContext, AttributeSet aAttrs) { - super(aContext, aAttrs); - initialize(aContext); - } - - public NoInternetWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { - super(aContext, aAttrs, aDefStyle); - initialize(aContext); - } - - private void initialize(Context aContext) { - inflate(aContext, R.layout.no_internet, this); - - Button mAcceptButton = findViewById(R.id.acceptButton); - mAcceptButton.setOnClickListener(view -> { - view.requestFocusFromTouch(); - hide(REMOVE_WIDGET); - }); - } - - @Override - protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { - Context context = getContext(); - aPlacement.width = WidgetPlacement.dpDimension(context, R.dimen.no_internet_width); - aPlacement.height = WidgetPlacement.dpDimension(context, R.dimen.no_internet_height); - aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.base_app_dialog_z_distance); - aPlacement.anchorX = 0.5f; - aPlacement.anchorY = 0.5f; - aPlacement.parentAnchorX = 0.5f; - aPlacement.parentAnchorY = 0.5f; - aPlacement.opaque = false; - aPlacement.visible = false; - } - - @Override - public void show(int aShowFlags) { - super.show(aShowFlags); - - mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); - } - - @Override - public void hide(int aHideFlags) { - super.hide(aHideFlags); - - mWidgetManager.popWorldBrightness(this); - } - -} 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 7fae8ae32..7e9bce4d4 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 @@ -1,7 +1,12 @@ package org.mozilla.vrbrowser.ui.widgets; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; +import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.Typeface; +import android.net.Uri; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.StyleSpan; @@ -12,20 +17,22 @@ import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import android.widget.AdapterView; import android.widget.ArrayAdapter; -import android.widget.ImageButton; import android.widget.ImageView; -import android.widget.ListView; import android.widget.TextView; import androidx.annotation.NonNull; -import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.audio.AudioEngine; import org.mozilla.vrbrowser.ui.views.CustomListView; +import org.mozilla.vrbrowser.ui.widgets.dialogs.SelectionActionWidget; +import org.mozilla.vrbrowser.utils.ViewUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class SuggestionsWidget extends UIWidget implements WidgetManagerDelegate.FocusChangeListener { @@ -37,10 +44,12 @@ public class SuggestionsWidget extends UIWidget implements WidgetManagerDelegate private URLBarPopupDelegate mURLBarDelegate; private String mHighlightedText; private AudioEngine mAudio; + private ClipboardManager mClipboard; + private SelectionActionWidget mSelectionMenu; public interface URLBarPopupDelegate { - default void OnItemClicked(SuggestionItem item) {}; - default void OnItemDeleted(SuggestionItem item) {}; + default void OnItemClicked(SuggestionItem item) {} + default void OnItemLongClicked(SuggestionItem item) {} } public SuggestionsWidget(Context aContext) { @@ -86,8 +95,12 @@ public void onAnimationRepeat(Animation animation) { mAdapter = new SuggestionsAdapter(getContext(), R.layout.list_popup_window_item, new ArrayList<>()); mList.setAdapter(mAdapter); + mList.setOnItemClickListener(mClickListener); + mList.setOnItemLongClickListener(mLongClickListener); + mList.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> hideMenu()); mAudio = AudioEngine.fromContext(aContext); + mClipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); mHighlightedText = ""; } @@ -125,11 +138,16 @@ public void hide(@HideFlags int aHideFlags) { mList.startAnimation(mScaleDownAnimation); } + public void hideNoAnim(@HideFlags int aHideFlags) { + super.hide(aHideFlags); + } + // FocusChangeListener @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { - if (oldFocus != null && isVisible()) { + if (!ViewUtils.isEqualOrChildrenOf(this, newFocus)) { + hideMenu(); onDismiss(); } } @@ -195,7 +213,6 @@ private class ItemViewHolder { ImageView favicon; TextView title; TextView url; - ImageButton delete; View divider; } @@ -215,24 +232,18 @@ public View getView(int position, View convertView, ViewGroup parent) { itemViewHolder.layout = listItem.findViewById(R.id.layout); itemViewHolder.layout.setTag(R.string.position_tag, position); - itemViewHolder.layout.setOnClickListener(mRowListener); itemViewHolder.favicon = listItem.findViewById(R.id.favicon); itemViewHolder.title = listItem.findViewById(R.id.title); itemViewHolder.url = listItem.findViewById(R.id.url); - itemViewHolder.delete = listItem.findViewById(R.id.delete); - itemViewHolder.delete.setTag(R.string.position_tag, position); - itemViewHolder.delete.setOnClickListener(mDeleteButtonListener); itemViewHolder.divider = listItem.findViewById(R.id.divider); listItem.setTag(R.string.list_item_view_tag, itemViewHolder); listItem.setOnHoverListener(mHoverListener); - listItem.setOnTouchListener(mTouchListener); } else { itemViewHolder = (ItemViewHolder) listItem.getTag(R.string.list_item_view_tag); itemViewHolder.layout.setTag(R.string.position_tag, position); - itemViewHolder.delete.setTag(R.string.position_tag, position); } SuggestionItem selectedItem = getItem(position); @@ -269,7 +280,6 @@ public View getView(int position, View convertView, ViewGroup parent) { itemViewHolder.favicon.setImageResource(R.drawable.ic_icon_bookmark); } - itemViewHolder.delete.setVisibility(GONE); itemViewHolder.favicon.setVisibility(VISIBLE); if (position == 0) { @@ -281,59 +291,6 @@ public View getView(int position, View convertView, ViewGroup parent) { return listItem; } - OnClickListener mDeleteButtonListener = v -> { - if (mAudio != null) { - mAudio.playSound(AudioEngine.Sound.CLICK); - } - - int position = (Integer)v.getTag(R.string.position_tag); - SuggestionItem item = getItem(position); - mAdapter.remove(item); - mAdapter.notifyDataSetChanged(); - - if (mURLBarDelegate != null) { - mURLBarDelegate.OnItemDeleted(item); - } - }; - - OnClickListener mRowListener = v -> { - if (mAudio != null) { - mAudio.playSound(AudioEngine.Sound.CLICK); - } - - hide(KEEP_WIDGET); - - requestFocus(); - requestFocusFromTouch(); - - if (mURLBarDelegate != null) { - int position = (Integer)v.getTag(R.string.position_tag); - SuggestionItem item = getItem(position); - mURLBarDelegate.OnItemClicked(item); - } - }; - - private OnTouchListener mTouchListener = (view, event) -> { - int position = (int)view.getTag(R.string.position_tag); - if (!isEnabled(position)) { - return false; - } - - int ev = event.getActionMasked(); - switch (ev) { - case MotionEvent.ACTION_UP: - view.setPressed(false); - view.performClick(); - return true; - - case MotionEvent.ACTION_DOWN: - view.setPressed(true); - return true; - } - - return false; - }; - private OnHoverListener mHoverListener = (view, motionEvent) -> { int position = (int)view.getTag(R.string.position_tag); if (!isEnabled(position)) { @@ -343,7 +300,6 @@ public View getView(int position, View convertView, ViewGroup parent) { View favicon = view.findViewById(R.id.favicon); TextView title = view.findViewById(R.id.title); View url = view.findViewById(R.id.url); - View delete = view.findViewById(R.id.delete); int ev = motionEvent.getActionMasked(); switch (ev) { case MotionEvent.ACTION_HOVER_ENTER: @@ -352,7 +308,6 @@ public View getView(int position, View convertView, ViewGroup parent) { title.setHovered(true); title.setShadowLayer(title.getShadowRadius(), title.getShadowDx(), title.getShadowDy(), getContext().getColor(R.color.text_shadow_light)); url.setHovered(true); - delete.setHovered(true); return true; case MotionEvent.ACTION_HOVER_EXIT: @@ -361,7 +316,6 @@ public View getView(int position, View convertView, ViewGroup parent) { title.setHovered(false); title.setShadowLayer(title.getShadowRadius(), title.getShadowDx(), title.getShadowDy(), getContext().getColor(R.color.text_shadow)); url.setHovered(false); - delete.setHovered(false); return true; } @@ -369,6 +323,90 @@ public View getView(int position, View convertView, ViewGroup parent) { }; } + private AdapterView.OnItemClickListener mClickListener = new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (mAudio != null) { + mAudio.playSound(AudioEngine.Sound.CLICK); + } + + hide(KEEP_WIDGET); + + requestFocus(); + requestFocusFromTouch(); + + if (mURLBarDelegate != null) { + SuggestionItem item = mAdapter.getItem(position); + mURLBarDelegate.OnItemClicked(item); + } + } + }; + + private AdapterView.OnItemLongClickListener mLongClickListener = new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + SuggestionItem item = mAdapter.getItem(position); + + view.setHovered(true); + + hideMenu(); + if (item != null) { + showMenu(view, item); + + return true; + } + + return false; + } + }; + + private void showMenu(@NonNull View view, @NonNull SuggestionItem item) { + if (mSelectionMenu == null) { + mSelectionMenu = new SelectionActionWidget(getContext()); + mSelectionMenu.mWidgetPlacement.parentHandle = getHandle(); + mSelectionMenu.setActions(Collections.singleton(GeckoSession.SelectionActionDelegate.ACTION_COPY)); + } + + Rect offsetViewBounds = new Rect(); + view.getDrawingRect(offsetViewBounds); + float ratio = WidgetPlacement.viewToWidgetRatio(getContext(), this); + offsetDescendantRectToMyCoords(view, offsetViewBounds); + RectF rectF = new RectF( + offsetViewBounds.left * ratio, + offsetViewBounds.top * ratio, + offsetViewBounds.right * ratio, + offsetViewBounds.bottom * ratio + ); + mSelectionMenu.setSelectionRect(rectF); + mSelectionMenu.setDelegate(new SelectionActionWidget.Delegate() { + @Override + public void onAction(String action) { + hideMenu(); + ClipData clip = ClipData.newRawUri(item.title, Uri.parse(item.url)); + mClipboard.setPrimaryClip(clip); + } + + @Override + public void onDismiss() { + hideMenu(); + } + }); + mSelectionMenu.show(KEEP_FOCUS); + } + + private void hideMenu() { + if (mSelectionMenu != null) { + mSelectionMenu.setDelegate((SelectionActionWidget.Delegate)null); + if (!mSelectionMenu.isReleased()) { + if (mSelectionMenu.isVisible()) { + mSelectionMenu.hide(REMOVE_WIDGET); + } + mSelectionMenu.releaseWidget(); + } + mSelectionMenu = null; + } + } + private SpannableStringBuilder createHighlightedText(@NonNull String text) { final SpannableStringBuilder sb = new SpannableStringBuilder(text); final StyleSpan bold = new StyleSpan(Typeface.BOLD); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TabsWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TabsWidget.java index d706fc89b..008354366 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TabsWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TabsWidget.java @@ -20,11 +20,10 @@ import org.mozilla.vrbrowser.ui.widgets.dialogs.SendTabDialogWidget; import org.mozilla.vrbrowser.ui.widgets.dialogs.UIDialog; import org.mozilla.vrbrowser.utils.BitmapCache; -import org.mozilla.vrbrowser.utils.ViewUtils; import java.util.ArrayList; -public class TabsWidget extends UIDialog implements WidgetManagerDelegate.WorldClickListener { +public class TabsWidget extends UIDialog { protected BitmapCache mBitmapCache; protected RecyclerView mTabsList; protected GridLayoutManager mLayoutManager; @@ -136,8 +135,6 @@ private void initialize() { mAdapter.notifyDataSetChanged(); updateSelectionMode(); }); - - mWidgetManager.addWorldClickListener(this); } public void attachToWindow(WindowWidget aWindow) { @@ -147,9 +144,6 @@ public void attachToWindow(WindowWidget aWindow) { @Override public void releaseWidget() { - if (mWidgetManager != null) { - mWidgetManager.removeWorldClickListener(this); - } super.releaseWidget(); } @@ -158,15 +152,15 @@ public void show(int aShowFlags) { super.show(aShowFlags); refreshTabs(); invalidate(); - mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); mTabsList.requestFocusFromTouch(); } @Override public void hide(@HideFlags int aHideFlags) { super.hide(aHideFlags); - mRenderer.clearSurface(); - mWidgetManager.popWorldBrightness(this); + if (mRenderer != null) { + mRenderer.clearSurface(); + } } public void setTabDelegate(TabDelegate aDelegate) { @@ -277,15 +271,11 @@ public void onAdd(TabView aSender) { @Override public void onSend(TabView aSender) { - hide(KEEP_WIDGET); - mSendTabDialog = new SendTabDialogWidget(getContext()); + if (mSendTabDialog == null) { + mSendTabDialog = new SendTabDialogWidget(getContext()); + } mSendTabDialog.setSessionId(aSender.getSession().getId()); mSendTabDialog.mWidgetPlacement.parentHandle = mWidgetManager.getFocusedWindow().getHandle(); - mSendTabDialog.setDelegate(() -> { - mSendTabDialog.releaseWidget(); - mSendTabDialog = null; - TabsWidget.this.show(REQUEST_FOCUS); - }); mSendTabDialog.show(UIWidget.REQUEST_FOCUS); } }); @@ -378,19 +368,4 @@ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, Recycle } } - @Override - public void onGlobalFocusChanged(View oldFocus, View newFocus) { - if (ViewUtils.isEqualOrChildrenOf(this, oldFocus) && this.isVisible() && - !ViewUtils.isEqualOrChildrenOf(this, newFocus)) { - onDismiss(); - } - } - - @Override - public void onWorldClick() { - if (this.isVisible()) { - onDismiss(); - } - } - } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TitleBarWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TitleBarWidget.java index 47d7ffd40..e03c2e934 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TitleBarWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TitleBarWidget.java @@ -167,7 +167,8 @@ public void setURL(String urlString) { } public void setIsInsecure(boolean aIsInsecure) { - if (mAttachedWindow.getSession().getCurrentUri() != null && + if (mAttachedWindow != null && mAttachedWindow.getSession() != null && + mAttachedWindow.getSession().getCurrentUri() != null && !(mAttachedWindow.getSession().getCurrentUri().startsWith("data") && mAttachedWindow.getSession().isPrivateMode())) { mBinding.insecureIcon.setVisibility(aIsInsecure ? View.VISIBLE : View.GONE); @@ -182,7 +183,7 @@ public void mediaAvailabilityChanged(boolean available) { if (mMedia != null) { mMedia.removeMediaListener(mMediaDelegate); } - if (available) { + if (available && mAttachedWindow != null && mAttachedWindow.getSession() != null) { mMedia = mAttachedWindow.getSession().getFullScreenVideo(); if (mMedia != null) { mMedia.addMediaListener(mMediaDelegate); @@ -192,10 +193,18 @@ public void mediaAvailabilityChanged(boolean available) { } } } else { + mMedia = null; mBinding.setIsMediaAvailable(false); } } + public void updateMediaStatus() { + if (mMedia != null) { + mBinding.setIsMediaAvailable(mMedia.isPlayed()); + mBinding.setIsMediaPlaying(mMedia.isPlaying()); + } + } + MediaElement.Delegate mMediaDelegate = new MediaElement.Delegate() { @Override public void onPlaybackStateChange(@NonNull MediaElement mediaElement, int state) { 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 e1eb5b57a..a951b1784 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 @@ -26,7 +26,6 @@ import org.mozilla.vrbrowser.browser.SessionChangeListener; import org.mozilla.vrbrowser.browser.engine.Session; import org.mozilla.vrbrowser.browser.engine.SessionStore; -import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.views.UIButton; import org.mozilla.vrbrowser.ui.widgets.settings.SettingsWidget; @@ -152,8 +151,6 @@ private void initialize(Context aContext) { view.requestFocusFromTouch(); - TelemetryWrapper.trayNewWindowEvent(); - notifyAddWindowClicked(); }); mAddWindowButton.setCurvedTooltip(false); @@ -391,7 +388,7 @@ public void toggleSettingsDialog() { toggleSettingsDialog(SettingsWidget.SettingDialog.MAIN); } - public void toggleSettingsDialog(SettingsWidget.SettingDialog settingDialog) { + public void toggleSettingsDialog(@NonNull SettingsWidget.SettingDialog settingDialog) { UIWidget widget = getChild(mSettingsDialogHandle); if (widget == null) { widget = createChild(SettingsWidget.class, false); @@ -408,6 +405,20 @@ public void toggleSettingsDialog(SettingsWidget.SettingDialog settingDialog) { } } + public void showSettingsDialog(@NonNull SettingsWidget.SettingDialog settingDialog) { + UIWidget widget = getChild(mSettingsDialogHandle); + if (widget == null) { + widget = createChild(SettingsWidget.class, false); + mSettingsDialogHandle = widget.getHandle(); + } + + if (mAttachedWindow != null) { + widget.getPlacement().parentHandle = mAttachedWindow.getHandle(); + } + + ((SettingsWidget)widget).show(REQUEST_FOCUS, settingDialog); + } + public void setTrayVisible(boolean aVisible) { if (mTrayVisible != aVisible) { mTrayVisible = aVisible; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UISurfaceTextureRenderer.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UISurfaceTextureRenderer.java index fc6f53c24..df45f433f 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UISurfaceTextureRenderer.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UISurfaceTextureRenderer.java @@ -12,12 +12,24 @@ import android.graphics.SurfaceTexture; import android.view.Surface; -class UISurfaceTextureRenderer { +import androidx.annotation.Nullable; + +public class UISurfaceTextureRenderer { private int mTextureWidth; private int mTextureHeight; private SurfaceTexture mSurfaceTexture; private Surface mSurface; private Canvas mSurfaceCanvas; + private static boolean sUseHarwareAcceleration; + private static boolean sRenderActive = true; + + public static void setUseHardwareAcceleration(boolean aEnabled) { + sUseHarwareAcceleration = aEnabled; + } + + public static void setRenderActive(boolean aActive) { + sRenderActive = aActive; + } UISurfaceTextureRenderer(SurfaceTexture aTexture, int aWidth, int aHeight) { mTextureWidth = aWidth; @@ -58,11 +70,19 @@ void release() { mSurfaceTexture = null; } + @Nullable Canvas drawBegin() { mSurfaceCanvas = null; + if (!sRenderActive) { + return null; + } if (mSurface != null) { try { - mSurfaceCanvas = mSurface.lockHardwareCanvas(); + if (sUseHarwareAcceleration) { + mSurfaceCanvas = mSurface.lockHardwareCanvas(); + } else { + mSurfaceCanvas = mSurface.lockCanvas(null); + } mSurfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); } catch (Exception e){ 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 6473ca535..4affab3ec 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 @@ -318,9 +318,9 @@ public void show(@ShowFlags int aShowFlags) { mWidgetManager.pushBackHandler(mBackHandler); } - setFocusableInTouchMode(false); + setFocusableInTouchMode(true); if (aShowFlags == REQUEST_FOCUS) { - requestFocusFromTouch(); + post(this::requestFocusFromTouch); } else if (aShowFlags == CLEAR_FOCUS) { clearFocus(); } 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 88ab047dd..7b9437386 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 @@ -6,7 +6,6 @@ import androidx.annotation.NonNull; import org.mozilla.geckoview.GeckoSession; -import org.mozilla.vrbrowser.VRBrowserActivity; import org.mozilla.vrbrowser.ui.widgets.menus.VideoProjectionMenuWidget; import org.mozilla.vrbrowser.utils.ConnectivityReceiver; @@ -88,4 +87,5 @@ interface WorldClickListener { TrayWidget getTray(); void addConnectivityListener(ConnectivityReceiver.Delegate aListener); void removeConnectivityListener(ConnectivityReceiver.Delegate aListener); + void saveState(); } 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 0a81be469..0c6cd4935 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 @@ -10,7 +10,9 @@ import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.SurfaceTexture; +import android.net.Uri; import android.util.Log; import android.util.Pair; import android.view.KeyEvent; @@ -20,26 +22,24 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.UiThread; -import org.jetbrains.annotations.NotNull; -import org.mozilla.geckoview.GeckoDisplay; -import org.mozilla.geckoview.GeckoResponse; import org.mozilla.geckoview.GeckoResult; import org.mozilla.geckoview.GeckoSession; import org.mozilla.geckoview.PanZoomController; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.VRBrowserApplication; -import org.mozilla.vrbrowser.browser.HistoryStore; import org.mozilla.vrbrowser.browser.PromptDelegate; import org.mozilla.vrbrowser.browser.SessionChangeListener; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.browser.VideoAvailabilityListener; import org.mozilla.vrbrowser.browser.engine.Session; import org.mozilla.vrbrowser.browser.engine.SessionStore; +import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.adapters.Bookmark; import org.mozilla.vrbrowser.ui.callbacks.BookmarksCallback; @@ -47,26 +47,21 @@ import org.mozilla.vrbrowser.ui.callbacks.LibraryItemContextMenuClickCallback; import org.mozilla.vrbrowser.ui.views.BookmarksView; import org.mozilla.vrbrowser.ui.views.HistoryView; -import org.mozilla.vrbrowser.ui.widgets.dialogs.BaseAppDialogWidget; -import org.mozilla.vrbrowser.ui.widgets.dialogs.ClearCacheDialogWidget; -import org.mozilla.vrbrowser.ui.widgets.dialogs.MessageDialogWidget; +import org.mozilla.vrbrowser.ui.widgets.dialogs.ClearHistoryDialogWidget; +import org.mozilla.vrbrowser.ui.widgets.dialogs.PromptDialogWidget; import org.mozilla.vrbrowser.ui.widgets.dialogs.SelectionActionWidget; import org.mozilla.vrbrowser.ui.widgets.menus.ContextMenuWidget; import org.mozilla.vrbrowser.ui.widgets.menus.LibraryMenuWidget; -import org.mozilla.vrbrowser.ui.widgets.prompts.AlertPromptWidget; -import org.mozilla.vrbrowser.ui.widgets.prompts.ConfirmPromptWidget; -import org.mozilla.vrbrowser.ui.widgets.prompts.PromptWidget; import org.mozilla.vrbrowser.ui.widgets.settings.SettingsWidget; -import org.mozilla.vrbrowser.utils.ConnectivityReceiver; -import org.mozilla.vrbrowser.utils.SystemUtils; import org.mozilla.vrbrowser.utils.ViewUtils; import java.util.ArrayList; import java.util.Arrays; -import java.util.Calendar; -import java.util.GregorianCalendar; +import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; +import java.util.stream.Collectors; +import java.util.stream.Stream; import mozilla.components.concept.storage.PageObservation; import mozilla.components.concept.storage.PageVisit; @@ -90,6 +85,12 @@ default void onBookmarksShown(WindowWidget aWindow) {} default void onBookmarksHidden(WindowWidget aWindow) {} } + @IntDef(value = { SESSION_RELEASE_DISPLAY, SESSION_DO_NOT_RELEASE_DISPLAY}) + public @interface OldSessionDisplayAction {} + public static final int SESSION_RELEASE_DISPLAY = 0; + public static final int SESSION_DO_NOT_RELEASE_DISPLAY = 1; + + private Surface mSurface; private int mWidth; private int mHeight; @@ -98,11 +99,10 @@ default void onBookmarksHidden(WindowWidget aWindow) {} private TopBarWidget mTopBar; private TitleBarWidget mTitleBar; private WidgetManagerDelegate mWidgetManager; - private AlertPromptWidget mAlertPrompt; - private ConfirmPromptWidget mConfirmPrompt; - private NoInternetWidget mNoInternetToast; - private MessageDialogWidget mAppDialog; - private ClearCacheDialogWidget mClearCacheDialog; + private PromptDialogWidget mAlertDialog; + private PromptDialogWidget mConfirmDialog; + private PromptDialogWidget mAppDialog; + private ClearHistoryDialogWidget mClearHistoryDialog; private ContextMenuWidget mContextMenu; private SelectionActionWidget mSelectionMenu; private LibraryMenuWidget mLibraryItemContextMenu; @@ -201,6 +201,7 @@ private void initialize(Context aContext) { setFocusable(true); TelemetryWrapper.openWindowEvent(mWindowId); + GleanMetricsService.openWindowEvent(mWindowId); if (mSession.getGeckoSession() != null) { onCurrentSessionChange(null, mSession.getGeckoSession()); @@ -234,8 +235,6 @@ void setupListeners(Session aSession) { aSession.addProgressListener(this); aSession.setHistoryDelegate(this); aSession.addSelectionActionListener(this); - - mWidgetManager.addConnectivityListener(mConnectivityDelegate); } void cleanListeners(Session aSession) { @@ -246,8 +245,6 @@ void cleanListeners(Session aSession) { aSession.removeProgressListener(this); aSession.setHistoryDelegate(null); aSession.removeSelectionActionListener(this); - - mWidgetManager.removeConnectivityListener(mConnectivityDelegate); } @Override @@ -313,6 +310,7 @@ public void onResume() { public void close() { TelemetryWrapper.closeWindowEvent(mWindowId); + GleanMetricsService.closeWindowEvent(mWindowId); hideContextMenus(); releaseWidget(); mBookmarksView.onDestroy(); @@ -327,23 +325,6 @@ public void close() { mListeners.clear(); } - private ConnectivityReceiver.Delegate mConnectivityDelegate = connected -> { - if (mActive) { - if (mNoInternetToast == null) { - mNoInternetToast = new NoInternetWidget(getContext()); - mNoInternetToast.mWidgetPlacement.parentHandle = getHandle(); - mNoInternetToast.mWidgetPlacement.parentAnchorY = 0.0f; - mNoInternetToast.mWidgetPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.base_app_dialog_y_distance); - } - if (!connected && !mNoInternetToast.isVisible()) { - mNoInternetToast.show(REQUEST_FOCUS); - - } else if (connected && mNoInternetToast.isVisible()) { - mNoInternetToast.hide(REMOVE_WIDGET); - } - } - }; - public void loadHomeIfNotRestored() { if (!mIsRestored) { loadHome(); @@ -649,6 +630,7 @@ public void setActiveWindow(boolean active) { updateTitleBar(); } + updateTitleBarMediaStatus(); hideContextMenus(); TelemetryWrapper.activePlacementEvent(mWindowPlacement.getValue(), mActive); @@ -693,6 +675,13 @@ private void updateTitleBarUrl(String url) { } } + public void updateTitleBarMediaStatus() { + if (mTitleBar != null) { + mTitleBar.updateMediaStatus(); + } + } + + @Nullable public Session getSession() { return mSession; } @@ -925,6 +914,17 @@ public void removeWindowListener(WindowListener aListener) { mListeners.remove(aListener); } + public void waitForFirstPaint() { + setFirstPaintReady(false); + setFirstDrawCallback(() -> { + if (!isFirstPaintReady()) { + setFirstPaintReady(true); + mWidgetManager.updateWidget(WindowWidget.this); + } + }); + mWidgetManager.updateWidget(this); + } + @Override public void handleResizeEvent(float aWorldWidth, float aWorldHeight) { int width = getWindowWidth(aWorldWidth); @@ -978,7 +978,7 @@ public void setFirstDrawCallback(Runnable aRunnable) { @Override public boolean isFirstPaintReady() { - return mWidgetPlacement.composited; + return mWidgetPlacement != null && mWidgetPlacement.composited; } @Override @@ -991,6 +991,7 @@ public boolean isLayer() { return mSurface != null && mTexture == null; } + @Override public void setVisible(boolean aVisible) { if (mWidgetPlacement.visible == aVisible) { @@ -1030,11 +1031,17 @@ public void draw(Canvas aCanvas) { } public void setSession(@NonNull Session aSession) { + setSession(aSession, SESSION_RELEASE_DISPLAY); + } + + public void setSession(@NonNull Session aSession, @OldSessionDisplayAction int aDisplayAction) { if (mSession != aSession) { Session oldSession = mSession; if (oldSession != null) { cleanListeners(oldSession); - oldSession.releaseDisplay(); + if (aDisplayAction == SESSION_RELEASE_DISPLAY) { + oldSession.releaseDisplay(); + } } mSession = aSession; @@ -1048,6 +1055,7 @@ public void setSession(@NonNull Session aSession) { listener.onSessionChanged(oldSession, aSession); } } + mCaptureOnPageStop = false; hideLibraryPanels(); } @@ -1082,24 +1090,27 @@ public void onCurrentSessionChange(GeckoSession aOldSession, GeckoSession aSessi } else { setPrivateBrowsingEnabled(false); } + waitForFirstPaint(); } @Override public void onStackSession(Session aSession) { // e.g. tab opened via window.open() + aSession.updateLastUse(); Session current = mSession; setSession(aSession); SessionStore.get().setActiveSession(aSession); - current.setActive(false); - current.captureBackgroundBitmap(getWindowWidth(), getWindowHeight()); + current.captureBackgroundBitmap(getWindowWidth(), getWindowHeight()).thenAccept(aVoid -> current.setActive(false)); mWidgetManager.getTray().showTabAddedNotification(); + + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.BROWSER); } @Override public void onUnstackSession(Session aSession, Session aParent) { if (mSession == aSession) { - setSession(aParent); aParent.setActive(true); + setSession(aParent); SessionStore.get().setActiveSession(aParent); SessionStore.get().destroySession(aSession); } @@ -1118,7 +1129,7 @@ public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { @Override public boolean onCheckIsTextEditor() { - return mSession.isInputActive(); + return !mIsResizing && mSession.isInputActive(); } @@ -1181,95 +1192,88 @@ public boolean onTouchEvent(MotionEvent aEvent) { @Override public boolean onGenericMotionEvent(MotionEvent aEvent) { - GeckoSession session = mSession.getGeckoSession(); - return (session != null) && session.getPanZoomController().onMotionEvent(aEvent) == PanZoomController.INPUT_RESULT_HANDLED; + if (mView != null) { + return super.onGenericMotionEvent(aEvent); + } else { + GeckoSession session = mSession.getGeckoSession(); + return (session != null) && session.getPanZoomController().onMotionEvent(aEvent) == PanZoomController.INPUT_RESULT_HANDLED; + } } private void setPrivateBrowsingEnabled(boolean isEnabled) { } - public void showAlert(String title, @NonNull String msg, @NonNull PromptWidget.PromptDelegate callback) { - mAlertPrompt = new AlertPromptWidget(getContext()); - mAlertPrompt.mWidgetPlacement.parentHandle = getHandle(); - mAlertPrompt.mWidgetPlacement.parentAnchorY = 0.0f; - mAlertPrompt.mWidgetPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.base_app_dialog_y_distance); - mAlertPrompt.setTitle(title); - mAlertPrompt.setMessage(msg); - mAlertPrompt.setPromptDelegate(callback); - mAlertPrompt.show(REQUEST_FOCUS); - } - - public void showButtonPrompt(String title, @NonNull String msg, @NonNull String[] btnMsg, @NonNull ConfirmPromptWidget.ConfirmPromptDelegate callback) { - mConfirmPrompt = new ConfirmPromptWidget(getContext()); - mConfirmPrompt.mWidgetPlacement.parentHandle = getHandle(); - mConfirmPrompt.mWidgetPlacement.parentAnchorY = 0.0f; - mConfirmPrompt.mWidgetPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.base_app_dialog_y_distance); - mConfirmPrompt.setTitle(title); - mConfirmPrompt.setMessage(msg); - mConfirmPrompt.setButtons(btnMsg); - mConfirmPrompt.setPromptDelegate(callback); - mConfirmPrompt.show(REQUEST_FOCUS); - } - - public void showAppDialog(@NonNull String title, @NonNull @StringRes int description, @NonNull @StringRes int [] btnMsg, - @NonNull BaseAppDialogWidget.Delegate buttonsCallback, @NonNull MessageDialogWidget.Delegate messageCallback) { - mAppDialog = new MessageDialogWidget(getContext()); - mAppDialog.mWidgetPlacement.parentHandle = getHandle(); - mAppDialog.mWidgetPlacement.parentAnchorY = 0.0f; - mAppDialog.mWidgetPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.base_app_dialog_y_distance); + public void showAlert(String title, @NonNull String msg, @Nullable PromptDialogWidget.Delegate callback) { + if (mAlertDialog == null) { + mAlertDialog = new PromptDialogWidget(getContext()); + mAlertDialog.setButtons(new int[] { + R.string.ok_button + }); + mAlertDialog.setCheckboxVisible(false); + mAlertDialog.setDescriptionVisible(false); + } + mAlertDialog.setTitle(title); + mAlertDialog.setBody(msg); + mAlertDialog.setButtonsDelegate(index -> { + mAlertDialog.hide(REMOVE_WIDGET); + if (callback != null) { + callback.onButtonClicked(index); + } + }); + mAlertDialog.show(REQUEST_FOCUS); + } + + public void showConfirmPrompt(String title, @NonNull String msg, @NonNull String[] btnMsg, @Nullable PromptDialogWidget.Delegate callback) { + if (mConfirmDialog == null) { + mConfirmDialog = new PromptDialogWidget(getContext()); + mAlertDialog.setButtons(new int[] { + R.string.cancel_button, + R.string.ok_button + }); + mConfirmDialog.setCheckboxVisible(false); + mConfirmDialog.setDescriptionVisible(false); + } + mConfirmDialog.setTitle(title); + mConfirmDialog.setBody(msg); + mConfirmDialog.setButtons(btnMsg); + mAlertDialog.setButtonsDelegate(index -> { + mAlertDialog.hide(REMOVE_WIDGET); + if (callback != null) { + callback.onButtonClicked(index); + } + }); + mConfirmDialog.show(REQUEST_FOCUS); + } + + public void showDialog(@NonNull String title, @NonNull @StringRes int description, @NonNull @StringRes int [] btnMsg, + @Nullable PromptDialogWidget.Delegate buttonsCallback, @Nullable Runnable linkCallback) { + mAppDialog = new PromptDialogWidget(getContext()); + mAppDialog.setIconVisible(false); + mAppDialog.setCheckboxVisible(false); + mAppDialog.setDescriptionVisible(false); mAppDialog.setTitle(title); - mAppDialog.setMessage(description); + mAppDialog.setBody(description); mAppDialog.setButtons(btnMsg); - mAppDialog.setButtonsDelegate(buttonsCallback); - mAppDialog.setMessageDelegate(messageCallback); + mAppDialog.setButtonsDelegate(index -> { + mAppDialog.hide(REMOVE_WIDGET); + if (buttonsCallback != null) { + buttonsCallback.onButtonClicked(index); + } + }); + mAppDialog.setLinkDelegate(() -> { + mAppDialog.hide(REMOVE_WIDGET); + if (linkCallback != null) { + linkCallback.run(); + } + }); mAppDialog.show(REQUEST_FOCUS); } public void showClearCacheDialog() { - mClearCacheDialog = new ClearCacheDialogWidget(getContext()); - mClearCacheDialog.mWidgetPlacement.parentHandle = getHandle(); - mClearCacheDialog.mWidgetPlacement.parentAnchorY = 0.0f; - mClearCacheDialog.mWidgetPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.base_app_dialog_y_distance); - mClearCacheDialog.setTitle(R.string.history_clear); - mClearCacheDialog.setButtons(new int[] { - R.string.history_clear_cancel, - R.string.history_clear_now - }); - mClearCacheDialog.setButtonsDelegate((index) -> { - if (index == BaseAppDialogWidget.NEGATIVE) { - mClearCacheDialog.hide(REMOVE_WIDGET); - - } else { - Calendar date = new GregorianCalendar(); - date.set(Calendar.HOUR_OF_DAY, 0); - date.set(Calendar.MINUTE, 0); - date.set(Calendar.SECOND, 0); - date.set(Calendar.MILLISECOND, 0); - - long currentTime = System.currentTimeMillis(); - long todayLimit = date.getTimeInMillis(); - long yesterdayLimit = todayLimit - SystemUtils.ONE_DAY_MILLIS; - long oneWeekLimit = todayLimit - SystemUtils.ONE_WEEK_MILLIS; - - HistoryStore store = SessionStore.get().getHistoryStore(); - switch (mClearCacheDialog.getSelectedRange()) { - case ClearCacheDialogWidget.TODAY: - store.deleteVisitsBetween(todayLimit, currentTime); - break; - case ClearCacheDialogWidget.YESTERDAY: - store.deleteVisitsBetween(yesterdayLimit, currentTime); - break; - case ClearCacheDialogWidget.LAST_WEEK: - store.deleteVisitsBetween(oneWeekLimit, currentTime); - break; - case ClearCacheDialogWidget.EVERYTHING: - store.deleteEverything(); - break; - } - SessionStore.get().purgeSessionHistory(); - } - }); - mClearCacheDialog.show(REQUEST_FOCUS); + if (mClearHistoryDialog == null) { + mClearHistoryDialog = new ClearHistoryDialogWidget(getContext()); + } + mClearHistoryDialog.show(REQUEST_FOCUS); } public void setMaxWindowScale(float aScale) { @@ -1309,7 +1313,7 @@ private int getWindowWidth(float aWorldWidth) { return (int) Math.floor(SettingsStore.WINDOW_WIDTH_DEFAULT * aWorldWidth / WidgetPlacement.floatDimension(getContext(), R.dimen.window_world_width)); } - private void showLibraryItemContextMenu(@NotNull View view, LibraryMenuWidget.LibraryContextMenuItem item, boolean isLastVisibleItem) { + private void showLibraryItemContextMenu(@NonNull View view, LibraryMenuWidget.LibraryContextMenuItem item, boolean isLastVisibleItem) { view.requestFocusFromTouch(); hideContextMenus(); @@ -1350,6 +1354,11 @@ public void onOpenInNewWindowClick(LibraryMenuWidget.LibraryContextMenuItem item @Override public void onOpenInNewTabClick(LibraryMenuWidget.LibraryContextMenuItem item) { mWidgetManager.openNewTabForeground(item.getUrl()); + if (item.getType() == LibraryMenuWidget.LibraryItemType.HISTORY) { + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.HISTORY); + } else if (item.getType() == LibraryMenuWidget.LibraryItemType.BOOKMARKS) { + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.BOOKMARKS); + } hideContextMenus(); } @@ -1376,7 +1385,7 @@ public void onRemoveFromBookmarks(LibraryMenuWidget.LibraryContextMenuItem item) private BookmarksCallback mBookmarksListener = new BookmarksCallback() { @Override - public void onShowContextMenu(@NonNull View view, @NotNull Bookmark item, boolean isLastVisibleItem) { + public void onShowContextMenu(@NonNull View view, @NonNull Bookmark item, boolean isLastVisibleItem) { showLibraryItemContextMenu( view, new LibraryMenuWidget.LibraryContextMenuItem( @@ -1388,7 +1397,22 @@ public void onShowContextMenu(@NonNull View view, @NotNull Bookmark item, boolea @Override public void onFxASynSettings(@NonNull View view) { - mWidgetManager.getTray().toggleSettingsDialog(SettingsWidget.SettingDialog.FXA); + mWidgetManager.getTray().showSettingsDialog(SettingsWidget.SettingDialog.FXA); + } + + @Override + public void onHideContextMenu(@NonNull View view) { + hideContextMenus(); + } + + @Override + public void onFxALogin(@NonNull View view) { + hideBookmarks(); + } + + @Override + public void onClickItem(@NonNull View view, Bookmark item) { + hideBookmarks(); } }; @@ -1412,7 +1436,22 @@ public void onShowContextMenu(@NonNull View view, @NonNull VisitInfo item, boole @Override public void onFxASynSettings(@NonNull View view) { - mWidgetManager.getTray().toggleSettingsDialog(SettingsWidget.SettingDialog.FXA); + mWidgetManager.getTray().showSettingsDialog(SettingsWidget.SettingDialog.FXA); + } + + @Override + public void onHideContextMenu(@NonNull View view) { + hideContextMenus(); + } + + @Override + public void onFxALogin(@NonNull View view) { + hideHistory(); + } + + @Override + public void onClickItem(@NonNull View view, @NonNull VisitInfo item) { + hideHistory(); } }; @@ -1455,7 +1494,6 @@ public void onContextMenu(GeckoSession session, int screenX, int screenY, Contex if (element.type == ContextElement.TYPE_VIDEO) { return; } - TelemetryWrapper.longPressContextMenuEvent(); hideContextMenus(); @@ -1558,28 +1596,86 @@ public GeckoResult onVisited(@NonNull GeckoSession geckoSession, @NonNu return GeckoResult.fromValue(false); } + // Check if we want this type of url. + if (!shouldStoreUri(url)) { + return GeckoResult.fromValue(false); + } + boolean isReload = lastVisitedURL != null && lastVisitedURL.equals(url); - PageVisit pageVisit; + VisitType visitType; if (isReload) { - pageVisit = new PageVisit(VisitType.RELOAD, RedirectSource.NOT_A_SOURCE); + visitType = VisitType.RELOAD; } else { - if ((flags & VISIT_REDIRECT_SOURCE_PERMANENT) != 0) { - pageVisit = new PageVisit(VisitType.REDIRECT_PERMANENT, RedirectSource.NOT_A_SOURCE); - } else if ((flags & VISIT_REDIRECT_SOURCE) != 0) { - pageVisit = new PageVisit(VisitType.REDIRECT_TEMPORARY, RedirectSource.NOT_A_SOURCE); + // Note the difference between `VISIT_REDIRECT_PERMANENT`, + // `VISIT_REDIRECT_TEMPORARY`, `VISIT_REDIRECT_SOURCE`, and + // `VISIT_REDIRECT_SOURCE_PERMANENT`. + // + // The former two indicate if the visited page is the *target* + // of a redirect; that is, another page redirected to it. + // + // The latter two indicate if the visited page is the *source* + // of a redirect: it's redirecting to another page, because the + // server returned an HTTP 3xy status code. + if ((flags & VISIT_REDIRECT_PERMANENT) != 0) { + visitType = VisitType.REDIRECT_PERMANENT; + + } else if ((flags & VISIT_REDIRECT_TEMPORARY) != 0) { + visitType = VisitType.REDIRECT_TEMPORARY; + } else { - pageVisit = new PageVisit(VisitType.LINK, RedirectSource.NOT_A_SOURCE); + visitType = VisitType.LINK; } } + RedirectSource redirectSource; + if ((flags & GeckoSession.HistoryDelegate.VISIT_REDIRECT_SOURCE_PERMANENT) != 0) { + redirectSource = RedirectSource.PERMANENT; + + } else if ((flags & GeckoSession.HistoryDelegate.VISIT_REDIRECT_SOURCE) != 0) { + redirectSource = RedirectSource.TEMPORARY; + + } else { + redirectSource = RedirectSource.NOT_A_SOURCE; + } - SessionStore.get().getHistoryStore().recordVisit(url, pageVisit); + SessionStore.get().getHistoryStore().recordVisit(url, new PageVisit(visitType, redirectSource)); SessionStore.get().getHistoryStore().recordObservation(url, new PageObservation(url)); return GeckoResult.fromValue(true); } + /** + * Filter out unwanted URIs, such as "chrome:", "about:", etc. + * Ported from nsAndroidHistory::CanAddURI + * See https://dxr.mozilla.org/mozilla-central/source/mobile/android/components/build/nsAndroidHistory.cpp#326 + */ + private boolean shouldStoreUri(@NonNull String uri) { + Uri parsedUri = Uri.parse(uri); + String scheme = parsedUri.getScheme(); + if (scheme == null) { + return false; + } + + // Short-circuit most common schemes. + if (scheme.equals("http") || scheme.equals("https")) { + return true; + } + + // Allow about about:reader uris. They are of the form: + // about:reader?url=http://some.interesting.page/to/read.html + if (uri.startsWith("about:reader")) { + return true; + } + + List schemasToIgnore = Stream.of( + "about", "imap", "news", "mailbox", "moz-anno", "moz-extension", + "view-source", "chrome", "resource", "data", "javascript", "blob" + ).collect(Collectors.toList()); + + return !schemasToIgnore.contains(scheme); + } + @UiThread @Nullable public GeckoResult getVisited(@NonNull GeckoSession geckoSession, @NonNull String[] urls) { @@ -1618,33 +1714,51 @@ public void onSecurityChange(GeckoSession geckoSession, SecurityInformation secu // GeckoSession.SelectionActionDelegate @Override - public void onShowActionRequest(@NonNull GeckoSession aSession, @NonNull Selection aSelection, @NonNull String[] aActions, @NonNull GeckoResponse aResponse) { - if (aActions.length == 1 && GeckoSession.SelectionActionDelegate.ACTION_HIDE.equals(aActions[0])) { + public void onShowActionRequest(@NonNull GeckoSession aSession, @NonNull Selection aSelection) { + if (aSelection.availableActions.size() == 1 && (aSelection.availableActions.contains(GeckoSession.SelectionActionDelegate.ACTION_HIDE))) { // See: https://github.com/MozillaReality/FirefoxReality/issues/2214 - aResponse.respond(GeckoSession.SelectionActionDelegate.ACTION_HIDE); + aSelection.hide(); return; } - TelemetryWrapper.longPressContextMenuEvent(); hideContextMenus(); mSelectionMenu = new SelectionActionWidget(getContext()); mSelectionMenu.mWidgetPlacement.parentHandle = getHandle(); - mSelectionMenu.setActions(aActions); + mSelectionMenu.setActions(aSelection.availableActions); Matrix matrix = new Matrix(); aSession.getClientToSurfaceMatrix(matrix); matrix.mapRect(aSelection.clientRect); - mSelectionMenu.setSelectionRect(aSelection.clientRect); + RectF selectionRect = null; + if (aSelection.clientRect != null) { + float ratio = WidgetPlacement.worldToWindowRatio(getContext()); + selectionRect = new RectF( + aSelection.clientRect.left * ratio, + aSelection.clientRect.top* ratio, + aSelection.clientRect.right * ratio, + aSelection.clientRect.bottom * ratio + ); + } + mSelectionMenu.setSelectionRect(selectionRect); mSelectionMenu.setDelegate(new SelectionActionWidget.Delegate() { @Override public void onAction(String action) { hideContextMenus(); - aResponse.respond(action); + if (aSelection.isActionAvailable(action)) { + aSelection.execute(action); + } + if (GeckoSession.SelectionActionDelegate.ACTION_COPY.equals(action) && + aSelection.isActionAvailable(GeckoSession.SelectionActionDelegate.ACTION_UNSELECT)) { + // Don't keep the text selected after it's copied. + aSelection.execute(GeckoSession.SelectionActionDelegate.ACTION_UNSELECT); + } } @Override public void onDismiss() { hideContextMenus(); - aResponse.respond(GeckoSession.SelectionActionDelegate.ACTION_UNSELECT); + if (aSelection.isActionAvailable(GeckoSession.SelectionActionDelegate.ACTION_UNSELECT)) { + aSelection.execute(GeckoSession.SelectionActionDelegate.ACTION_UNSELECT); + } } }); mSelectionMenu.show(KEEP_FOCUS); 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 index d57bf0afa..572d0a78a 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java @@ -10,7 +10,6 @@ import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; -import org.jetbrains.annotations.NotNull; import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.VRBrowserApplication; @@ -21,9 +20,13 @@ 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.ui.widgets.settings.SettingsWidget; import org.mozilla.vrbrowser.utils.BitmapCache; +import org.mozilla.vrbrowser.utils.ConnectivityReceiver; import org.mozilla.vrbrowser.utils.SystemUtils; import java.io.File; @@ -103,6 +106,7 @@ class WindowsState { private TabsWidget mTabsWidget; private Accounts mAccounts; private Services mServices; + private PromptDialogWidget mNoInternetDialog; public enum WindowPlacement{ FRONT(0), @@ -142,10 +146,12 @@ public Windows(Context aContext) { mServices = ((VRBrowserApplication)mContext.getApplicationContext()).getServices(); mServices.setTabReceivedDelegate(this); + mWidgetManager.addConnectivityListener(mConnectivityDelegate); + restoreWindows(); } - private void saveState() { + public void saveState() { File file = new File(mContext.getFilesDir(), WINDOWS_SAVE_FILENAME); try (Writer writer = new FileWriter(file)) { WindowsState state = new WindowsState(); @@ -201,6 +207,7 @@ public WindowWidget getFocusedWindow() { return mFocusedWindow; } + @NonNull public WindowWidget addWindow() { if (getCurrentWindows().size() >= MAX_WINDOWS) { return null; @@ -248,6 +255,11 @@ public WindowWidget addWindow() { focusWindow(newWindow); updateCurvedMode(true); updateViews(); + + // We are only interested in general windows opened. + if (!isInPrivateMode()) { + GleanMetricsService.newWindowOpenEvent(); + } return newWindow; } @@ -279,12 +291,12 @@ public void closeWindow(@NonNull WindowWidget aWindow) { if (leftWindow == aWindow) { removeWindow(leftWindow); - if (mFocusedWindow == leftWindow) { + if (mFocusedWindow == leftWindow && frontWindow != null) { focusWindow(frontWindow); } } else if (rightWindow == aWindow) { removeWindow(rightWindow); - if (mFocusedWindow == rightWindow) { + if (mFocusedWindow == rightWindow && frontWindow != null) { focusWindow(frontWindow); } } else if (frontWindow == aWindow) { @@ -295,7 +307,7 @@ public void closeWindow(@NonNull WindowWidget aWindow) { placeWindow(leftWindow, WindowPlacement.FRONT); } - if (mFocusedWindow == frontWindow && !getCurrentWindows().isEmpty()) { + if (mFocusedWindow == frontWindow && !getCurrentWindows().isEmpty() && getFrontWindow() != null) { focusWindow(getFrontWindow()); } @@ -326,7 +338,7 @@ public void moveWindowRight(@NonNull WindowWidget aWindow) { WindowWidget leftWindow = getLeftWindow(); WindowWidget rightWindow = getRightWindow(); - if (aWindow == leftWindow) { + if (aWindow == leftWindow && frontWindow != null) { placeWindow(leftWindow, WindowPlacement.FRONT); placeWindow(frontWindow, WindowPlacement.LEFT); switchTopBars(leftWindow, frontWindow); @@ -351,7 +363,7 @@ public void moveWindowLeft(@NonNull WindowWidget aWindow) { WindowWidget leftWindow = getLeftWindow(); WindowWidget rightWindow = getRightWindow(); - if (aWindow == rightWindow) { + if (aWindow == rightWindow && frontWindow != null) { placeWindow(rightWindow, WindowPlacement.FRONT); placeWindow(frontWindow, WindowPlacement.RIGHT); switchTopBars(rightWindow, frontWindow); @@ -371,7 +383,7 @@ public void moveWindowLeft(@NonNull WindowWidget aWindow) { } } - public void focusWindow(@NonNull WindowWidget aWindow) { + public void focusWindow(@Nullable WindowWidget aWindow) { if (aWindow != mFocusedWindow) { WindowWidget prev = mFocusedWindow; mFocusedWindow = aWindow; @@ -411,23 +423,13 @@ public void onPause() { mIsPaused = true; saveState(); - for (WindowWidget window: mRegularWindows) { - window.onPause(); - } - for (WindowWidget window: mPrivateWindows) { - window.onPause(); - } } public void onResume() { mIsPaused = false; - for (WindowWidget window: mRegularWindows) { - window.onResume(); - } - for (WindowWidget window: mPrivateWindows) { - window.onResume(); - } + TelemetryWrapper.resetOpenedWindowsCount(mRegularWindows.size(), false); + TelemetryWrapper.resetOpenedWindowsCount(mPrivateWindows.size(), true); } public void onDestroy() { @@ -440,6 +442,7 @@ public void onDestroy() { } mAccounts.removeAccountListener(mAccountObserver); mServices.setTabReceivedDelegate(null); + mWidgetManager.removeConnectivityListener(mConnectivityDelegate); } public boolean isInPrivateMode() { @@ -515,9 +518,7 @@ public void enterPrivateMode() { if (mPrivateWindows.size() == 0) { WindowWidget window = addWindow(); - if (window != null) { - window.loadHome(); - } + window.loadHome(); } else { focusWindow(getWindowWithPlacement(mPrivateWindowPlacement)); @@ -544,7 +545,10 @@ public void exitPrivateMode() { for (WindowWidget window: mRegularWindows) { setWindowVisible(window, true); } - focusWindow(getWindowWithPlacement(mRegularWindowPlacement)); + WindowWidget window = getWindowWithPlacement(mRegularWindowPlacement); + if (window != null) { + focusWindow(window); + } updateViews(); mWidgetManager.popWorldBrightness(this); } @@ -581,6 +585,7 @@ public ArrayList getCurrentWindows() { return mPrivateMode ? mPrivateWindows : mRegularWindows; } + @Nullable private WindowWidget getWindowWithPlacement(WindowPlacement aPlacement) { for (WindowWidget window: getCurrentWindows()) { if (window.getWindowPlacement() == aPlacement) { @@ -590,6 +595,7 @@ private WindowWidget getWindowWithPlacement(WindowPlacement aPlacement) { return null; } + @Nullable private WindowWidget getFrontWindow() { if (mFullscreenWindow != null) { return mFullscreenWindow; @@ -597,20 +603,26 @@ private WindowWidget getFrontWindow() { return getWindowWithPlacement(WindowPlacement.FRONT); } + @Nullable private WindowWidget getLeftWindow() { return getWindowWithPlacement(WindowPlacement.LEFT); } + @Nullable private WindowWidget getRightWindow() { return getWindowWithPlacement(WindowPlacement.RIGHT); } private void restoreWindows() { + boolean restoreEnabled = SettingsStore.getInstance(mContext).isRestoreTabsEnabled(); WindowsState windowsState = restoreState(); - if (windowsState != null) { + if (restoreEnabled && windowsState != null) { ArrayList restoredSessions = new ArrayList<>(); if (windowsState.tabs != null) { - windowsState.tabs.forEach(state -> restoredSessions.add(SessionStore.get().createSuspendedSession(state))); + windowsState.tabs.forEach(state -> { + restoredSessions.add(SessionStore.get().createSuspendedSession(state)); + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.PRE_EXISTING); + }); } mPrivateMode = false; for (WindowState windowState : windowsState.regularWindowsState) { @@ -667,6 +679,9 @@ private void removeWindow(@NonNull WindowWidget aWindow) { } 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.getTopBar().setVisible(aVisible); aWindow.getTitleBar().setVisible(aVisible); @@ -840,6 +855,7 @@ private void updateTitleBars() { } } + @NonNull private WindowWidget createWindow(@Nullable Session aSession) { int newWindowId = sIndex++; WindowWidget window; @@ -890,30 +906,31 @@ public void onLoggedOut() { } @Override - public void onAuthenticated(@NotNull OAuthAccount oAuthAccount, @NotNull AuthType authType) { - if (authType != AuthType.Existing.INSTANCE) { + 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: - getFocusedWindow().switchBookmarks(); + getFocusedWindow().showBookmarks(); break; case HISTORY: - getFocusedWindow().switchHistory(); + getFocusedWindow().showHistory(); break; case SETTINGS: - mWidgetManager.getTray().toggleSettingsDialog(SettingsWidget.SettingDialog.FXA); + mWidgetManager.getTray().showSettingsDialog(SettingsWidget.SettingDialog.FXA); break; } } } @Override - public void onProfileUpdated(@NotNull Profile profile) { + public void onProfileUpdated(@NonNull Profile profile) { } @@ -976,14 +993,7 @@ public void onTabsClicked() { private void setFirstPaint(@NonNull final WindowWidget aWindow, @NonNull final Session aSession) { if (aSession.getGeckoSession() == null) { - aWindow.setFirstPaintReady(false); - aWindow.setFirstDrawCallback(() -> { - if (!aWindow.isFirstPaintReady()) { - aWindow.setFirstPaintReady(true); - mWidgetManager.updateWidget(aWindow); - } - }); - mWidgetManager.updateWidget(aWindow); + 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 @@ -1024,7 +1034,9 @@ public void onMoveRightClicked(TopBarWidget aWidget) { // Title Bar Delegate @Override public void onTitleClicked(@NonNull TitleBarWidget titleBar) { - focusWindow(titleBar.getAttachedWindow()); + if (titleBar.getAttachedWindow() != null) { + focusWindow(titleBar.getAttachedWindow()); + } } @Override @@ -1087,12 +1099,12 @@ private WindowWidget getWindowWithSession(Session aSession) { // WindowWidget.Delegate @Override - public void onFocusRequest(WindowWidget aWindow) { + public void onFocusRequest(@NonNull WindowWidget aWindow) { focusWindow(aWindow); } @Override - public void onBorderChanged(WindowWidget aWindow) { + public void onBorderChanged(@NonNull WindowWidget aWindow) { if (mDelegate != null) { mDelegate.onWindowBorderChanged(aWindow); } @@ -1131,6 +1143,10 @@ public void onFullScreen(@NonNull WindowWidget aWindow, boolean aFullScreen) { @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) { @@ -1139,9 +1155,11 @@ public void onTabSelect(Session aTab) { Session moveTo = targetWindow.getSession(); moveFrom.surfaceDestroyed(); moveTo.surfaceDestroyed(); - windowToMove.setSession(moveTo); - targetWindow.setSession(moveFrom); + windowToMove.setSession(moveTo, WindowWidget.SESSION_DO_NOT_RELEASE_DISPLAY); + targetWindow.setSession(moveFrom, WindowWidget.SESSION_DO_NOT_RELEASE_DISPLAY); SessionStore.get().setActiveSession(targetWindow.getSession()); + windowToMove.setActiveWindow(false); + targetWindow.setActiveWindow(true); } else { setFirstPaint(targetWindow, aTab); @@ -1156,7 +1174,7 @@ public void addTab(WindowWidget targetWindow) { addTab(targetWindow, null); } - public void addTab(@NotNull WindowWidget targetWindow, @Nullable String aUri) { + public void addTab(@NonNull WindowWidget targetWindow, @Nullable String aUri) { Session session = SessionStore.get().createSuspendedSession(aUri, targetWindow.getSession().isPrivateMode()); setFirstPaint(targetWindow, session); targetWindow.getSession().setActive(false); @@ -1180,6 +1198,7 @@ public void addBackgroundTab(WindowWidget targetWindow, String aUri) { @Override public void onTabAdd() { addTab(mFocusedWindow, null); + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.TABS_DIALOG); } @Override @@ -1243,8 +1262,9 @@ public void onTabsClose(ArrayList aTabs) { } @Override - public void onTabsReceived(@NotNull List aTabs) { + public void onTabsReceived(@NonNull List 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()); @@ -1254,9 +1274,13 @@ public void onTabsReceived(@NotNull List aTabs) { 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. SessionStore.get().setActiveSession(session); + targetWindow.getSession().setActive(false); targetWindow.setSession(session); } } @@ -1269,4 +1293,27 @@ public void onTabsReceived(@NotNull List aTabs) { 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 -> { + mNoInternetDialog.hide(UIWidget.REMOVE_WIDGET); + }); + } + + if (!connected && !mNoInternetDialog.isVisible()) { + mNoInternetDialog.show(UIWidget.REQUEST_FOCUS); + + } else if (connected && mNoInternetDialog.isVisible()) { + mNoInternetDialog.hide(UIWidget.REMOVE_WIDGET); + } + }; } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/BaseAppDialogWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/BaseAppDialogWidget.java deleted file mode 100644 index 0135f9784..000000000 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/BaseAppDialogWidget.java +++ /dev/null @@ -1,162 +0,0 @@ -/* -*- 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.view.LayoutInflater; -import android.view.View; -import android.view.ViewTreeObserver; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.databinding.DataBindingUtil; - -import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.browser.engine.SessionStore; -import org.mozilla.vrbrowser.databinding.BaseAppDialogBinding; -import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; -import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; - -public class BaseAppDialogWidget extends UIDialog { - - public interface Delegate { - void onButtonClicked(int index); - default void onDismiss() {} - } - - public static final int NEGATIVE = 0; - public static final int POSITIVE = 1; - - protected BaseAppDialogBinding mBinding; - private Delegate mAppDialogDelegate; - private String mHelpLink; - - public BaseAppDialogWidget(Context aContext) { - super(aContext); - initialize(aContext); - } - - protected void initialize(Context aContext) { - LayoutInflater inflater = LayoutInflater.from(aContext); - - // Inflate this data binding layout - mBinding = DataBindingUtil.inflate(inflater, R.layout.base_app_dialog, this, true); - - mBinding.leftButton.setOnClickListener(v -> { - if (mAppDialogDelegate != null) { - mAppDialogDelegate.onButtonClicked(NEGATIVE); - } - - BaseAppDialogWidget.this.onDismiss(); - }); - mBinding.rightButton.setOnClickListener(v -> { - if (mAppDialogDelegate != null) { - mAppDialogDelegate.onButtonClicked(POSITIVE); - } - - BaseAppDialogWidget.this.onDismiss(); - }); - mBinding.helpButton.setOnClickListener(v -> SessionStore.get().getActiveSession().loadUri(mHelpLink)); - } - - @Override - protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { - aPlacement.visible = false; - aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.base_app_dialog_width); - aPlacement.height = WidgetPlacement.dpDimension(getContext(), R.dimen.base_app_dialog_height); - aPlacement.parentAnchorX = 0.5f; - aPlacement.parentAnchorY = 0.5f; - aPlacement.anchorX = 0.5f; - aPlacement.anchorY = 0.5f; - aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.base_app_dialog_z_distance); - } - - @Override - public void show(@ShowFlags int aShowFlags) { - measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - super.show(aShowFlags); - - mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); - - ViewTreeObserver viewTreeObserver = getViewTreeObserver(); - if (viewTreeObserver.isAlive()) { - viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - getViewTreeObserver().removeOnGlobalLayoutListener(this); - mWidgetPlacement.height = (int)(getHeight()/mWidgetPlacement.density); - mWidgetManager.updateWidget(BaseAppDialogWidget.this); - } - }); - } - } - - public void hide(@HideFlags int aHideFlags) { - super.hide(aHideFlags); - mWidgetManager.popWorldBrightness(this); - } - - // WidgetManagerDelegate.FocusChangeListener - - @Override - public void onGlobalFocusChanged(View oldFocus, View newFocus) { - if (oldFocus == this && isVisible() && findViewById(newFocus.getId()) == null) { - onDismiss(); - } - } - - public void setButtonsDelegate(Delegate delegate) { - mAppDialogDelegate = delegate; - } - - public void setTitle(@StringRes int title) { - mBinding.title.setText(title); - } - - public void setTitle(String title) { - mBinding.title.setText(title); - } - - public void setDescription(String title) { - mBinding.description.setVisibility(VISIBLE); - mBinding.description.setText(title); - } - - public void setDescription(@StringRes int title) { - mBinding.description.setVisibility(VISIBLE); - mBinding.description.setText(title); - } - - public void setHelpLink(@NonNull String text) { - mBinding.helpButton.setVisibility(VISIBLE); - mHelpLink = text; - } - - public void setHelpLink(@StringRes int textRes) { - mBinding.helpButton.setVisibility(VISIBLE); - mHelpLink = getResources().getString(textRes); - } - - public void setButtons(@NonNull @StringRes int[] buttons) { - if (buttons.length > 0) { - mBinding.leftButton.setText(buttons[NEGATIVE]); - } - if (buttons.length > 1) { - mBinding.rightButton.setText(buttons[POSITIVE]); - } - } - - public void setButtons(@NonNull String[] buttons) { - if (buttons.length > 0) { - mBinding.leftButton.setText(buttons[NEGATIVE]); - } - if (buttons.length > 1) { - mBinding.rightButton.setText(buttons[POSITIVE]); - } - } - -} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ClearCacheDialogWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ClearCacheDialogWidget.java deleted file mode 100644 index f2828dec3..000000000 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ClearCacheDialogWidget.java +++ /dev/null @@ -1,48 +0,0 @@ -/* -*- 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.view.LayoutInflater; - -import androidx.annotation.IntDef; -import androidx.databinding.DataBindingUtil; - -import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.databinding.ClearCacheDialogBinding; -import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; - -public class ClearCacheDialogWidget extends BaseAppDialogWidget { - - @IntDef(value = { TODAY, YESTERDAY, LAST_WEEK, EVERYTHING}) - public @interface ClearCacheRange {} - public static final int TODAY = 0; - public static final int YESTERDAY = 1; - public static final int LAST_WEEK = 2; - public static final int EVERYTHING = 3; - - private ClearCacheDialogBinding mClearCacheBinding; - - public ClearCacheDialogWidget(Context aContext) { - super(aContext); - } - - @Override - protected void initialize(Context aContext) { - super.initialize(aContext); - - LayoutInflater inflater = LayoutInflater.from(aContext); - - // Inflate this data binding layout - mClearCacheBinding = DataBindingUtil.inflate(inflater, R.layout.clear_cache_dialog, mBinding.dialogContent, true); - mClearCacheBinding.clearCacheRadio.setChecked(0, false); - } - - public @ClearCacheRange int getSelectedRange() { - return mClearCacheBinding.clearCacheRadio.getCheckedRadioButtonId(); - } - -} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ClearHistoryDialogWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ClearHistoryDialogWidget.java new file mode 100644 index 000000000..aebda99a5 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/ClearHistoryDialogWidget.java @@ -0,0 +1,85 @@ +/* -*- 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.view.LayoutInflater; + +import androidx.databinding.DataBindingUtil; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.browser.HistoryStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; +import org.mozilla.vrbrowser.databinding.ClearHistoryDialogBinding; +import org.mozilla.vrbrowser.utils.SystemUtils; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +public class ClearHistoryDialogWidget extends SettingDialogWidget { + + public static final int TODAY = 0; + public static final int YESTERDAY = 1; + public static final int LAST_WEEK = 2; + public static final int EVERYTHING = 3; + + private ClearHistoryDialogBinding mClearHistoryBinding; + + public ClearHistoryDialogWidget(Context aContext) { + super(aContext); + } + + @Override + protected void initialize(Context aContext) { + super.initialize(aContext); + + LayoutInflater inflater = LayoutInflater.from(aContext); + + // Inflate this data binding layout + mClearHistoryBinding = DataBindingUtil.inflate(inflater, R.layout.clear_history_dialog, mBinding.content, true); + mClearHistoryBinding.clearHistoryRadio.setChecked(0, false); + + mBinding.headerLayout.setTitle(R.string.history_clear); + mBinding.footerLayout.setFooterButtonText(R.string.history_clear_now); + mBinding.footerLayout.setFooterButtonClickListener((view -> { + Calendar date = new GregorianCalendar(); + date.set(Calendar.HOUR_OF_DAY, 0); + date.set(Calendar.MINUTE, 0); + date.set(Calendar.SECOND, 0); + date.set(Calendar.MILLISECOND, 0); + + long currentTime = System.currentTimeMillis(); + long todayLimit = date.getTimeInMillis(); + long yesterdayLimit = todayLimit - SystemUtils.ONE_DAY_MILLIS; + long oneWeekLimit = todayLimit - SystemUtils.ONE_WEEK_MILLIS; + + HistoryStore store = SessionStore.get().getHistoryStore(); + switch (mClearHistoryBinding.clearHistoryRadio.getCheckedRadioButtonId()) { + case ClearHistoryDialogWidget.TODAY: + store.deleteVisitsBetween(todayLimit, currentTime); + break; + case ClearHistoryDialogWidget.YESTERDAY: + store.deleteVisitsBetween(yesterdayLimit, currentTime); + break; + case ClearHistoryDialogWidget.LAST_WEEK: + store.deleteVisitsBetween(oneWeekLimit, currentTime); + break; + case ClearHistoryDialogWidget.EVERYTHING: + store.deleteEverything(); + break; + } + SessionStore.get().purgeSessionHistory(); + onDismiss(); + })); + } + + @Override + public void show(int aShowFlags) { + super.show(aShowFlags); + + mClearHistoryBinding.clearHistoryRadio.setChecked(0, false); + } +} 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 769c831c6..acb0f0446 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 @@ -6,141 +6,83 @@ package org.mozilla.vrbrowser.ui.widgets.dialogs; import android.content.Context; -import android.util.AttributeSet; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.TextView; + +import androidx.annotation.NonNull; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.audio.AudioEngine; -import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.browser.SettingsStore; -import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; -import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; +import org.mozilla.vrbrowser.utils.SystemUtils; -public class CrashDialogWidget extends UIDialog { +import java.util.ArrayList; - public interface CrashDialogDelegate { - void onSendData(); - default void onDoNotSendData() {} - } +public class CrashDialogWidget extends PromptDialogWidget { - private Button mLearnMoreButton; - private Button mDoNotSendButton; - private Button mSendDataButton; - private CheckBox mSendDataCheckBox; - private AudioEngine mAudio; - private CrashDialogDelegate mCrashDialogDelegate; - private TextView mCrashMessage; + private String mDumpFile; + private String mExtraFile; + private ArrayList mFiles; - public CrashDialogWidget(Context aContext) { + public CrashDialogWidget(@NonNull Context aContext, @NonNull String dumpFile, @NonNull String extraFile) { super(aContext); - initialize(aContext); - } - public CrashDialogWidget(Context aContext, AttributeSet aAttrs) { - super(aContext, aAttrs); - initialize(aContext); - } + mDumpFile = dumpFile; + mExtraFile = extraFile; - public CrashDialogWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { - super(aContext, aAttrs, aDefStyle); initialize(aContext); } - private void initialize(Context aContext) { - inflate(aContext, R.layout.crash_dialog, this); - - mWidgetManager.addFocusChangeListener(this); - - mLearnMoreButton = findViewById(R.id.learnMoreButton); - mDoNotSendButton = findViewById(R.id.dontSendButton); - mSendDataButton = findViewById(R.id.sendDataButton); - mSendDataCheckBox = findViewById(R.id.crashSendDataCheckbox); - mCrashMessage = findViewById(R.id.crashMessage); - - mLearnMoreButton.setOnClickListener(view -> { - if (mAudio != null) { - mAudio.playSound(AudioEngine.Sound.CLICK); - } + public CrashDialogWidget(@NonNull Context aContext, @NonNull ArrayList files) { + super(aContext); - SessionStore.get().getActiveSession().loadUri(getContext().getString(R.string.crash_dialog_learn_more_url)); + mFiles = files; - onDismiss(); - }); + initialize(aContext); + } - mDoNotSendButton.setOnClickListener(view -> { - if (mAudio != null) { - mAudio.playSound(AudioEngine.Sound.CLICK); - } + @Override + protected void initialize(Context aContext) { + super.initialize(aContext); - if(mCrashDialogDelegate != null) { - mCrashDialogDelegate.onDoNotSendData(); - } - onDismiss(); + setButtons(new int[] { + R.string.do_not_sent_button, + R.string.send_data_button }); + setButtonsDelegate(index -> { + if (index == PromptDialogWidget.NEGATIVE) { + if (mFiles != null) { + SystemUtils.clearCrashFiles(getContext(), mFiles); + } + onDismiss(); - mSendDataButton.setOnClickListener(view -> { - if (mAudio != null) { - mAudio.playSound(AudioEngine.Sound.CLICK); - } + } else if (index == PromptDialogWidget.POSITIVE) { + if (mFiles != null) { + SystemUtils.postCrashFiles(getContext(), mFiles); - hide(REMOVE_WIDGET); + } else { + SystemUtils.postCrashFiles(getContext(), mDumpFile, mExtraFile); + } - if(mCrashDialogDelegate != null) { - mCrashDialogDelegate.onSendData(); - } + SettingsStore.getInstance(getContext()).setCrashReportingEnabled(mBinding.checkbox.isChecked()); - SettingsStore.getInstance(getContext()).setCrashReportingEnabled(mSendDataCheckBox.isChecked()); - }); - - mSendDataCheckBox.setChecked(SettingsStore.getInstance(getContext()).isCrashReportingEnabled()); - mSendDataCheckBox.setOnCheckedChangeListener((compoundButton, b) -> { - if (mAudio != null) { - mAudio.playSound(AudioEngine.Sound.CLICK); + onDismiss(); } }); - mCrashMessage.setText(getContext().getString(R.string.crash_dialog_message, getContext().getString(R.string.app_name))); + setDescriptionVisible(false); - mAudio = AudioEngine.fromContext(aContext); + setIcon(R.drawable.sad_fox); + setTitle(R.string.crash_dialog_heading); + setBody(getContext().getString(R.string.crash_dialog_message, getContext().getString(R.string.app_name))); + setCheckboxText(R.string.crash_dialog_send_data); + setLinkDelegate(() -> { + mWidgetManager.openNewTabForeground(getContext().getString(R.string.crash_dialog_learn_more_url)); + onDismiss(); + }); } @Override - public void releaseWidget() { - mWidgetManager.removeFocusChangeListener(this); - - super.releaseWidget(); - } + public void show(int aShowFlags) { + mBinding.checkbox.setChecked(false); - @Override - protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { - aPlacement.visible = false; - aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.crash_dialog_width); - aPlacement.height = WidgetPlacement.dpDimension(getContext(), R.dimen.crash_dialog_width); - aPlacement.parentAnchorX = 0.5f; - aPlacement.parentAnchorY = 0.5f; - aPlacement.anchorX = 0.5f; - aPlacement.anchorY = 0.5f; - aPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.crash_dialog_world_y); - aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.crash_dialog_world_z); - } - - @Override - public void show(@ShowFlags int aShowFlags) { super.show(aShowFlags); - - mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); - } - - @Override - public void hide(@HideFlags int aHideFlags) { - super.hide(aHideFlags); - - mWidgetManager.popWorldBrightness(this); - } - - public void setCrashDialogDelegate(CrashDialogDelegate aDelegate) { - mCrashDialogDelegate = aDelegate; } } 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 deleted file mode 100644 index cf3aec70f..000000000 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/MaxWindowsWidget.java +++ /dev/null @@ -1,40 +0,0 @@ -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/MessageDialogWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/MessageDialogWidget.java deleted file mode 100644 index 8ab5c56ac..000000000 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/MessageDialogWidget.java +++ /dev/null @@ -1,59 +0,0 @@ -/* -*- 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.view.LayoutInflater; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.databinding.DataBindingUtil; - -import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.databinding.MessageDialogBinding; -import org.mozilla.vrbrowser.utils.ViewUtils; - -public class MessageDialogWidget extends BaseAppDialogWidget { - - public interface Delegate { - void onMessageLinkClicked(@NonNull String url); - } - - private MessageDialogBinding mMessageBinding; - private Delegate mMessageDialogDelegate; - - public MessageDialogWidget(Context aContext) { - super(aContext); - } - - @Override - protected void initialize(Context aContext) { - super.initialize(aContext); - - LayoutInflater inflater = LayoutInflater.from(aContext); - - // Inflate this data binding layout - mMessageBinding = DataBindingUtil.inflate(inflater, R.layout.message_dialog, mBinding.dialogContent, true); - } - - public void setMessageDelegate(Delegate delegate) { - mMessageDialogDelegate = delegate; - } - - public void setMessage(@StringRes int message) { - ViewUtils.setTextViewHTML(mMessageBinding.message, getResources().getString(message), (widget, url) -> { - if (mMessageDialogDelegate != null) { - mMessageDialogDelegate.onMessageLinkClicked(url); - onDismiss(); - } - }); - } - - public void setMessage(String message) { - mMessageBinding.message.setText(message); - } - -} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/PermissionWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/PermissionWidget.java index e5e14b9be..8d84b1d51 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/PermissionWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/PermissionWidget.java @@ -8,26 +8,15 @@ import android.content.Context; import android.text.Spannable; import android.text.SpannableStringBuilder; -import android.util.AttributeSet; import android.util.Log; -import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; -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.SystemUtils; import java.net.URI; -public class PermissionWidget extends UIDialog implements WidgetManagerDelegate.FocusChangeListener { +public class PermissionWidget extends PromptDialogWidget { - private TextView mPermissionMessage; - private ImageView mPermissionIcon; private GeckoSession.PermissionDelegate.Callback mPermissionCallback; public enum PermissionType { @@ -44,81 +33,60 @@ public PermissionWidget(Context aContext) { initialize(aContext); } - public PermissionWidget(Context aContext, AttributeSet aAttrs) { - super(aContext, aAttrs); - initialize(aContext); - } - - public PermissionWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { - super(aContext, aAttrs, aDefStyle); - initialize(aContext); - } - - private void initialize(Context aContext) { - inflate(aContext, R.layout.permission, this); - - mPermissionIcon = findViewById(R.id.permissionIcon); - mPermissionMessage = findViewById(R.id.permissionText); - - Button cancelButton = findViewById(R.id.permissionCancelButton); - cancelButton.setOnClickListener(v -> handlePermissionResult(false)); - - Button allowButton = findViewById(R.id.permissionAllowButton); - allowButton.setOnClickListener(v -> handlePermissionResult(true)); - } - @Override - protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { - Context context = getContext(); - aPlacement.width = WidgetPlacement.dpDimension(context, R.dimen.permission_width); - aPlacement.height = WidgetPlacement.dpDimension(context, R.dimen.permission_height); - aPlacement.worldWidth = WidgetPlacement.floatDimension(context, R.dimen.permission_world_width); - aPlacement.translationZ = WidgetPlacement.unitFromMeters(context, R.dimen.browser_children_z_distance); - aPlacement.parentAnchorX = 0.5f; - aPlacement.parentAnchorY = 0.5f; - aPlacement.anchorX = 0.5f; - aPlacement.anchorY = 0.5f; - } - - @Override - public void show(@ShowFlags int aShowFlags) { - super.show(aShowFlags); - - mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); - } - - @Override - public void hide(@HideFlags int aHideFlag) { - super.hide(aHideFlag); - - mWidgetManager.popWorldBrightness(this); + protected void initialize(Context aContext) { + super.initialize(aContext); + + setButtons(new int[] { + R.string.permission_reject, + R.string.permission_allow + }); + setButtonsDelegate(index -> { + if (index == PromptDialogWidget.NEGATIVE) { + // Do not allow + handlePermissionResult(false); + + } else if (index == PromptDialogWidget.POSITIVE) { + // Allow + handlePermissionResult(true); + } + }); + setCheckboxVisible(false); + setDescriptionVisible(false); } public void showPrompt(String aUri, PermissionType aType, GeckoSession.PermissionDelegate.Callback aCallback) { + int titleId; int messageId; int iconId; switch (aType) { case Camera: + titleId = R.string.security_options_permission_camera; messageId = R.string.permission_camera; iconId = R.drawable.ic_icon_dialog_camera; break; case Microphone: + titleId = R.string.security_options_permission_microphone; messageId = R.string.permission_microphone; iconId = R.drawable.ic_icon_microphone; break; case CameraAndMicrophone: + titleId = R.string.security_options_permission_camera_and_microphone; messageId = R.string.permission_camera_and_microphone; iconId = R.drawable.ic_icon_dialog_camera; break; case Location: + titleId = R.string.security_options_permission_location; messageId = R.string.permission_location; iconId = R.drawable.ic_icon_dialog_geolocation; break; case Notification: + titleId = R.string.security_options_permission_notifications; messageId = R.string.permission_notification; iconId = R.drawable.ic_icon_dialog_notification; break; case ReadExternalStorage: + titleId = R.string.security_options_permission_storage; messageId = R.string.permission_read_external_storage; iconId = R.drawable.ic_icon_storage; break; @@ -137,8 +105,9 @@ public void showPrompt(String aUri, PermissionType aType, GeckoSession.Permissio SpannableStringBuilder str = new SpannableStringBuilder(message); str.setSpan(new android.text.style.StyleSpan(android.graphics.Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - mPermissionMessage.setText(str); - mPermissionIcon.setImageResource(iconId); + setIcon(iconId); + setTitle(titleId); + setBody(str); show(REQUEST_FOCUS); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/PopUpBlockDialogWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/PopUpBlockDialogWidget.java index a7574dc6e..e444c2727 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/PopUpBlockDialogWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/PopUpBlockDialogWidget.java @@ -6,16 +6,11 @@ package org.mozilla.vrbrowser.ui.widgets.dialogs; import android.content.Context; -import android.view.LayoutInflater; - -import androidx.databinding.DataBindingUtil; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.databinding.PopupBlockDialogBinding; -public class PopUpBlockDialogWidget extends BaseAppDialogWidget { +public class PopUpBlockDialogWidget extends PromptDialogWidget { - private PopupBlockDialogBinding mPopUpBinding; private boolean mIsChecked; public PopUpBlockDialogWidget(Context aContext) { @@ -26,17 +21,20 @@ public PopUpBlockDialogWidget(Context aContext) { protected void initialize(Context aContext) { super.initialize(aContext); - LayoutInflater inflater = LayoutInflater.from(aContext); - - // Inflate this data binding layout - mPopUpBinding = DataBindingUtil.inflate(inflater, R.layout.popup_block_dialog, mBinding.dialogContent, true); - mPopUpBinding.contentCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> mIsChecked = isChecked); + setDescriptionVisible(false); setButtons(new int[] { R.string.popup_block_button_cancel, R.string.popup_block_button_show }); + setIcon(R.drawable.ff_logo); + setTitle(String.format(getContext().getString(R.string.popup_block_title), getContext().getString(R.string.app_name))); + setBody(R.string.popup_block_description); + setCheckboxText(R.string.popup_block_checkbox); + + mBinding.checkbox.setOnCheckedChangeListener((buttonView, isChecked) -> mIsChecked = isChecked); + } public boolean askAgain() { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/PromptDialogWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/PromptDialogWidget.java new file mode 100644 index 000000000..239e28043 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/PromptDialogWidget.java @@ -0,0 +1,191 @@ +/* -*- 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.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.databinding.DataBindingUtil; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.databinding.PromptDialogBinding; +import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; +import org.mozilla.vrbrowser.utils.ViewUtils; + +public class PromptDialogWidget extends UIDialog { + + public interface Delegate { + void onButtonClicked(int index); + default void onDismiss() {} + } + + public static final int NEGATIVE = 0; + public static final int POSITIVE = 1; + + protected PromptDialogBinding mBinding; + private Delegate mAppDialogDelegate; + private Runnable mLinkDelegate; + + public PromptDialogWidget(Context aContext) { + super(aContext); + initialize(aContext); + } + + protected void initialize(Context aContext) { + LayoutInflater inflater = LayoutInflater.from(aContext); + + // Inflate this data binding layout + mBinding = DataBindingUtil.inflate(inflater, R.layout.prompt_dialog, this, true); + + mBinding.leftButton.setOnClickListener(v -> { + if (mAppDialogDelegate != null) { + mAppDialogDelegate.onButtonClicked(NEGATIVE); + } + }); + mBinding.rightButton.setOnClickListener(v -> { + if (mAppDialogDelegate != null) { + mAppDialogDelegate.onButtonClicked(POSITIVE); + } + }); + } + + @Override + protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { + // We align it at the same position as the settings panel + aPlacement.visible = false; + aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.prompt_dialog_width); + aPlacement.height = WidgetPlacement.dpDimension(getContext(), R.dimen.prompt_dialog_height); + aPlacement.anchorX = 0.5f; + aPlacement.anchorY = 0.5f; + aPlacement.parentAnchorY = 0.0f; + aPlacement.parentAnchorX = 0.5f; + aPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.settings_world_y) - + WidgetPlacement.unitFromMeters(getContext(), R.dimen.window_world_y); + aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.settings_world_z) - + WidgetPlacement.unitFromMeters(getContext(), R.dimen.window_world_z); + } + + @Override + public void show(@ShowFlags int aShowFlags) { + mWidgetPlacement.parentHandle = mWidgetManager.getFocusedWindow().getHandle(); + + super.show(aShowFlags); + } + + public void setButtonsDelegate(Delegate delegate) { + mAppDialogDelegate = delegate; + } + + public void setLinkDelegate(@NonNull Runnable delegate) { + mLinkDelegate = delegate; + } + + public void setIconVisible(boolean visible) { + mBinding.imageContainer.setVisibility(visible ? VISIBLE: GONE); + } + + public void setIcon(Drawable icon) { + mBinding.icon.setImageDrawable(icon); + } + + public void setIcon(@DrawableRes int icon) { + mBinding.icon.setImageResource(icon); + } + + public void setTitle(@StringRes int title) { + mBinding.title.setText(title); + } + + public void setTitle(String title) { + mBinding.title.setText(title); + } + + public void setBody(@NonNull String body) { + ViewUtils.setTextViewHTML(mBinding.body, body, (widget, url) -> { + if (mLinkDelegate != null) { + mLinkDelegate.run(); + } + }); + } + + public void setBody(@StringRes int body) { + ViewUtils.setTextViewHTML(mBinding.body, getResources().getString(body), (widget, url) -> { + if (mLinkDelegate != null) { + mLinkDelegate.run(); + } + }); + } + + public void setBody(@NonNull CharSequence body) { + mBinding.body.setText(body); + } + + public void setCheckboxVisible(boolean visible) { + mBinding.checkboxContainer.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + public void setCheckboxText(@StringRes int text) { + mBinding.checkbox.setText(text); + } + + public void setCheckboxText(String text) { + mBinding.checkbox.setText(text); + } + + public void setDescriptionVisible(boolean visible) { + mBinding.descriptionContainer.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + public void setDescription(@StringRes int description) { + mBinding.description.setText(description); + } + + public void setDescription(String description) { + mBinding.title.setText(description); + } + + public void setButtons(@NonNull @StringRes int[] buttons) { + if (buttons.length > 0) { + mBinding.leftButton.setText(buttons[NEGATIVE]); + + } else { + mBinding.leftButton.setVisibility(View.GONE); + } + + if (buttons.length > 1) { + mBinding.rightButton.setText(buttons[POSITIVE]); + + } else { + mBinding.rightButton.setVisibility(View.GONE); + } + } + + public void setButtons(@NonNull String[] buttons) { + if (buttons.length > 0) { + mBinding.leftButton.setText(buttons[NEGATIVE]); + + } else { + mBinding.leftButton.setVisibility(View.GONE); + } + + if (buttons.length > 1) { + mBinding.rightButton.setText(buttons[POSITIVE]); + + } else { + mBinding.rightButton.setVisibility(View.GONE); + } + } + + public boolean isChecked() { + return mBinding.checkbox.isChecked(); + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/RestartDialogWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/RestartDialogWidget.java index 9f4a2572a..b2eb3a0f5 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/RestartDialogWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/RestartDialogWidget.java @@ -6,85 +6,40 @@ package org.mozilla.vrbrowser.ui.widgets.dialogs; import android.content.Context; -import android.util.AttributeSet; -import android.widget.Button; -import android.widget.TextView; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.audio.AudioEngine; -import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; -import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; import org.mozilla.vrbrowser.utils.SystemUtils; -public class RestartDialogWidget extends UIDialog { - - private AudioEngine mAudio; +public class RestartDialogWidget extends PromptDialogWidget { public RestartDialogWidget(Context aContext) { super(aContext); initialize(aContext); } - public RestartDialogWidget(Context aContext, AttributeSet aAttrs) { - super(aContext, aAttrs); - initialize(aContext); - } - - public RestartDialogWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { - super(aContext, aAttrs, aDefStyle); - initialize(aContext); - } - - private void initialize(Context aContext) { - inflate(aContext, R.layout.restart_dialog, this); - - Button acceptButton = findViewById(R.id.restartNowButton); - Button cancelButton = findViewById(R.id.restartLaterButton); - TextView restartText = findViewById(R.id.restartText); - - acceptButton.setOnClickListener(view -> { - if (mAudio != null) { - mAudio.playSound(AudioEngine.Sound.CLICK); - } + @Override + protected void initialize(Context aContext) { + super.initialize(aContext); - postDelayed(() -> SystemUtils.scheduleRestart(getContext(), 100), 500); + setButtons(new int[] { + R.string.restart_later_dialog_button, + R.string.restart_now_dialog_button }); - cancelButton.setOnClickListener(view -> { - if (mAudio != null) { - mAudio.playSound(AudioEngine.Sound.CLICK); - } + setButtonsDelegate(index -> { + if (index == PromptDialogWidget.NEGATIVE) { + onDismiss(); - onDismiss(); + } else if (index == PromptDialogWidget.POSITIVE) { + mWidgetManager.saveState(); + postDelayed(() -> SystemUtils.scheduleRestart(getContext(), 100), 500); + } }); - restartText.setText(getContext().getString(R.string.restart_dialog_text, getContext().getString(R.string.app_name))); - - mAudio = AudioEngine.fromContext(aContext); - } + setCheckboxVisible(false); + setDescriptionVisible(false); - @Override - protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { - aPlacement.visible = false; - aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.restart_dialog_width); - aPlacement.height = WidgetPlacement.dpDimension(getContext(), R.dimen.restart_dialog_height); - aPlacement.parentAnchorX = 0.5f; - aPlacement.parentAnchorY = 0.5f; - aPlacement.anchorX = 0.5f; - aPlacement.anchorY = 0.5f; - aPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.restart_dialog_world_y); - aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.restart_dialog_world_z); + setIcon(R.drawable.ff_logo); + setTitle(R.string.restart_dialog_restart); + setBody(getContext().getString(R.string.restart_dialog_text, getContext().getString(R.string.app_name))); } - @Override - public void show(int aShowFlags) { - super.show(aShowFlags); - - mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); - } - - @Override - public void hide(int aHideFlags) { - super.hide(aHideFlags); - - mWidgetManager.popWorldBrightness(this); - } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SelectionActionWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SelectionActionWidget.java index f4d783f10..5986a852f 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SelectionActionWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SelectionActionWidget.java @@ -16,11 +16,10 @@ 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.StringUtils; import org.mozilla.vrbrowser.utils.ViewUtils; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collection; import static android.view.Gravity.CENTER_VERTICAL; @@ -34,7 +33,8 @@ public interface Delegate { private Point mPosition; private LinearLayout mContainer; private int mMinButtonWidth; - private String[] mActions; + private int mMaxButtonWidth; + private Collection mActions; public SelectionActionWidget(Context aContext) { super(aContext); @@ -44,7 +44,8 @@ public SelectionActionWidget(Context aContext) { private void initialize() { inflate(getContext(), R.layout.selection_action_menu, this); mContainer = findViewById(R.id.selectionMenuContainer); - mMinButtonWidth = WidgetPlacement.pixelDimension(getContext(), R.dimen.autocompletion_widget_min_item_width); + mMinButtonWidth = WidgetPlacement.pixelDimension(getContext(), R.dimen.selection_action_item_min_width); + mMaxButtonWidth = WidgetPlacement.pixelDimension(getContext(), R.dimen.selection_action_item_max_width); mBackHandler = () -> { onDismiss(); }; @@ -60,7 +61,7 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.anchorY = 0.5f; aPlacement.translationX = 0.0f; aPlacement.translationY = 0.0f; - aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.context_menu_z_distance); + aPlacement.translationZ = 1.0f; aPlacement.visible = false; } @@ -75,8 +76,8 @@ public void show(@ShowFlags int aShowFlags) { if (mPosition != null) { mWidgetPlacement.parentAnchorX = 0.0f; mWidgetPlacement.parentAnchorY = 1.0f; - mWidgetPlacement.translationX = mPosition.x * WidgetPlacement.worldToWindowRatio(getContext()); - mWidgetPlacement.translationY = -mPosition.y * WidgetPlacement.worldToWindowRatio(getContext()); + mWidgetPlacement.translationX = mPosition.x; + mWidgetPlacement.translationY = -mPosition.y; mWidgetPlacement.translationY += mWidgetPlacement.height * 0.5f; } super.show(aShowFlags); @@ -103,21 +104,21 @@ public void setSelectionRect(@Nullable RectF aRect) { } } - public void setActions(@NonNull String[] aActions) { + public void setActions(@NonNull Collection aActions) { mActions = aActions; mContainer.removeAllViews(); ArrayList buttons = new ArrayList<>(); - if (StringUtils.contains(aActions, GeckoSession.SelectionActionDelegate.ACTION_CUT)) { + if (aActions.contains(GeckoSession.SelectionActionDelegate.ACTION_CUT)) { buttons.add(createButton(R.string.context_menu_cut_text, GeckoSession.SelectionActionDelegate.ACTION_CUT, this::handleAction)); } - if (StringUtils.contains(aActions, GeckoSession.SelectionActionDelegate.ACTION_COPY)) { + if (aActions.contains(GeckoSession.SelectionActionDelegate.ACTION_COPY)) { buttons.add(createButton(R.string.context_menu_copy_text, GeckoSession.SelectionActionDelegate.ACTION_COPY, this::handleAction)); } - if (StringUtils.contains(aActions, GeckoSession.SelectionActionDelegate.ACTION_PASTE)) { + if (aActions.contains(GeckoSession.SelectionActionDelegate.ACTION_PASTE)) { buttons.add(createButton(R.string.context_menu_paste_text, GeckoSession.SelectionActionDelegate.ACTION_PASTE, this::handleAction)); } - if (StringUtils.contains(aActions, GeckoSession.SelectionActionDelegate.ACTION_SELECT_ALL)) { + if (aActions.contains(GeckoSession.SelectionActionDelegate.ACTION_SELECT_ALL)) { buttons.add(createButton(R.string.context_menu_select_all_text, GeckoSession.SelectionActionDelegate.ACTION_SELECT_ALL, this::handleAction)); } @@ -140,11 +141,11 @@ public void setActions(@NonNull String[] aActions) { } public boolean hasAction(String aAction) { - return mActions != null && StringUtils.contains(mActions, aAction); + return mActions != null && mActions.contains(aAction); } - public boolean hasSameActions(@NonNull String[] aActions) { - return Arrays.deepEquals(mActions, aActions); + public boolean hasSameActions(@NonNull Collection aActions) { + return (mActions != null) && mActions.containsAll(aActions) && (mActions.size() == aActions.size()); } private UITextButton createButton(int aStringId, String aAction, OnClickListener aHandler) { @@ -155,6 +156,7 @@ private UITextButton createButton(int aStringId, String aAction, OnClickListener } LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); button.setMinWidth(mMinButtonWidth); + button.setMaxWidth(mMaxButtonWidth); params.gravity = CENTER_VERTICAL; button.setLayoutParams(params); button.setTag(aAction); @@ -183,7 +185,7 @@ private void handleAction(View sender) { @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { - if (!ViewUtils.isChildrenOf(getChildAt(0), newFocus)) { + if (!ViewUtils.isEqualOrChildrenOf(this, newFocus)) { onDismiss(); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SendTabDialogWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SendTabDialogWidget.java index b8ad44517..7ed0ca98f 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SendTabDialogWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SendTabDialogWidget.java @@ -13,7 +13,6 @@ import androidx.annotation.Nullable; import androidx.databinding.DataBindingUtil; -import org.jetbrains.annotations.NotNull; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.VRBrowserApplication; import org.mozilla.vrbrowser.browser.Accounts; @@ -21,8 +20,6 @@ import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.databinding.SendTabsDisplayBinding; import org.mozilla.vrbrowser.ui.widgets.UIWidget; -import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; -import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; import java.util.ArrayList; import java.util.Collections; @@ -40,8 +37,7 @@ public class SendTabDialogWidget extends SettingDialogWidget implements DeviceConstellationObserver, - AccountObserver, - WidgetManagerDelegate.WorldClickListener { + AccountObserver { private SendTabsDisplayBinding mSendTabsDialogBinding; private Accounts mAccounts; @@ -65,46 +61,23 @@ protected void initialize(@NonNull Context aContext) { mSendTabsDialogBinding.setIsEmpty(false); mAccounts = ((VRBrowserApplication)getContext().getApplicationContext()).getAccounts(); - mAccounts.addAccountListener(this); - mAccounts.addDeviceConstellationListener(this); mBinding.headerLayout.setTitle(getResources().getString(R.string.send_tab_dialog_title)); mBinding.headerLayout.setDescription(R.string.send_tab_dialog_description); mBinding.footerLayout.setFooterButtonText(R.string.send_tab_dialog_button); mBinding.footerLayout.setFooterButtonClickListener(this::sendTabButtonClick); - - mWidgetPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.cache_app_dialog_width); - } - - @Override - protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { - super.initializeWidgetPlacement(aPlacement); - - mWidgetPlacement.parentAnchorY = 0.0f; - mWidgetPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.settings_world_y) - - WidgetPlacement.unitFromMeters(getContext(), R.dimen.window_world_y); - mWidgetPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.settings_world_z) - - WidgetPlacement.unitFromMeters(getContext(), R.dimen.window_world_z); - } - - @Override - public void releaseWidget() { - mAccounts.removeAccountListener(this); - mAccounts.removeDeviceConstellationListener(this); - - super.releaseWidget(); } @Override public void show(int aShowFlags) { + mAccounts.addAccountListener(this); + mAccounts.addDeviceConstellationListener(this); + if (mAccounts.isSignedIn()) { mBinding.footerLayout.setFooterButtonVisibility(View.GONE); mAccounts.refreshDevicesAsync(); mSendTabsDialogBinding.setIsSyncing(true); - mWidgetManager.addWorldClickListener(this); - mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); - super.show(aShowFlags); } else { @@ -116,8 +89,8 @@ public void show(int aShowFlags) { public void hide(int aHideFlags) { super.hide(aHideFlags); - mWidgetManager.popWorldBrightness(this); - mWidgetManager.removeWorldClickListener(this); + mAccounts.removeAccountListener(this); + mAccounts.removeDeviceConstellationListener(this); } public void setSessionId(@Nullable String sessionId) { @@ -143,32 +116,13 @@ private void sendTabButtonClick(View v) { private void showWhatsNewDialog() { mWhatsNew = new WhatsNewWidget(getContext()); mWhatsNew.setLoginOrigin(Accounts.LoginOrigin.SEND_TABS); - mWhatsNew.getPlacement().parentHandle = mWidgetManager.getFocusedWindow().getHandle(); - mWhatsNew.setStartBrowsingCallback(() -> { - mWhatsNew.hide(REMOVE_WIDGET); - mWhatsNew.releaseWidget(); - mWhatsNew = null; - onDismiss(); - }); - mWhatsNew.setSignInCallback(() -> { - mWhatsNew.hide(REMOVE_WIDGET); - mWhatsNew.releaseWidget(); - mWhatsNew = null; - hide(KEEP_WIDGET); - }); - mWhatsNew.setDelegate(() -> { - mWhatsNew.hide(REMOVE_WIDGET); - mWhatsNew.releaseWidget(); - mWhatsNew = null; - onDismiss(); - }); mWhatsNew.show(UIWidget.REQUEST_FOCUS); } // DeviceConstellationObserver @Override - public void onDevicesUpdate(@NotNull ConstellationState constellationState) { + public void onDevicesUpdate(@NonNull ConstellationState constellationState) { post(() -> { mSendTabsDialogBinding.setIsSyncing(false); @@ -203,14 +157,14 @@ public void onLoggedOut() { } @Override - public void onAuthenticated(@NotNull OAuthAccount oAuthAccount, @NotNull AuthType authType) { + public void onAuthenticated(@NonNull OAuthAccount oAuthAccount, @NonNull AuthType authType) { if (mAccounts.getLoginOrigin() == Accounts.LoginOrigin.SEND_TABS) { show(REQUEST_FOCUS); } } @Override - public void onProfileUpdated(@NotNull Profile profile) { + public void onProfileUpdated(@NonNull Profile profile) { } @@ -221,11 +175,4 @@ public void onAuthenticationProblems() { } showWhatsNewDialog(); } - - // WidgetManagerDelegate.WorldClickListener - - @Override - public void onWorldClick() { - onDismiss(); - } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SettingDialogWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SettingDialogWidget.java index 57de2bebe..f4da4982d 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SettingDialogWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SettingDialogWidget.java @@ -34,13 +34,23 @@ protected void initialize(Context aContext) { @Override protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.visible = false; - aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.base_app_dialog_width); - aPlacement.height = WidgetPlacement.pixelDimension(getContext(), R.dimen.browser_width_pixels)/2; - aPlacement.parentAnchorX = 0.5f; - aPlacement.parentAnchorY = 0.5f; + aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_width); + aPlacement.height = WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_height); aPlacement.anchorX = 0.5f; aPlacement.anchorY = 0.5f; - aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.base_app_dialog_z_distance); + aPlacement.parentAnchorX = 0.5f; + aPlacement.parentAnchorY = 0.0f; + aPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.settings_world_y) - + WidgetPlacement.unitFromMeters(getContext(), R.dimen.window_world_y); + aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.settings_world_z) - + WidgetPlacement.unitFromMeters(getContext(), R.dimen.window_world_z); + } + + @Override + public void show(@ShowFlags int aShowFlags) { + mWidgetPlacement.parentHandle = mWidgetManager.getFocusedWindow().getHandle(); + + super.show(aShowFlags); } protected void onFooterButton() { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SignOutDialogWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SignOutDialogWidget.java new file mode 100644 index 000000000..f84cce38e --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/SignOutDialogWidget.java @@ -0,0 +1,87 @@ +/* -*- 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.drawable.BitmapDrawable; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.VRBrowserApplication; +import org.mozilla.vrbrowser.browser.Accounts; +import org.mozilla.vrbrowser.browser.Places; +import org.mozilla.vrbrowser.ui.widgets.UIWidget; +import org.mozilla.vrbrowser.ui.widgets.settings.SettingsWidget; + +import java.util.Objects; +import java.util.concurrent.Executor; + +public class SignOutDialogWidget extends PromptDialogWidget { + + private Accounts mAccounts; + private Executor mUIThreadExecutor; + private Places mPlaces; + + public SignOutDialogWidget(Context aContext) { + super(aContext); + initialize(aContext); + } + + @Override + protected void initialize(Context aContext) { + super.initialize(aContext); + + mUIThreadExecutor = ((VRBrowserApplication)getContext().getApplicationContext()).getExecutors().mainThread(); + mAccounts = ((VRBrowserApplication)getContext().getApplicationContext()).getAccounts(); + mPlaces = ((VRBrowserApplication)getContext().getApplicationContext()).getPlaces(); + + setButtons(new int[] { + R.string.fxa_signout_confirmation_signout, + R.string.fxa_signout_confirmation_cancel + }); + setButtonsDelegate(index -> { + if (index == PromptDialogWidget.NEGATIVE) { + try { + Objects.requireNonNull(mAccounts.logoutAsync()).thenAcceptAsync(unit -> { + if (isChecked()) { + // Clear History and Bookmarks + mPlaces.clear(); + } + hide(UIWidget.REMOVE_WIDGET); + mWidgetManager.getTray().toggleSettingsDialog(); + + }, mUIThreadExecutor); + + } catch(NullPointerException e) { + e.printStackTrace(); + } + + } else if (index == PromptDialogWidget.POSITIVE) { + hide(UIWidget.REMOVE_WIDGET); + mWidgetManager.getTray().toggleSettingsDialog(SettingsWidget.SettingDialog.FXA); + } + }); + setDelegate(() -> mWidgetManager.getTray().toggleSettingsDialog(SettingsWidget.SettingDialog.FXA)); + + setDescriptionVisible(false); + + setTitle(R.string.fxa_signout_confirmation_title); + setBody(R.string.fxa_signout_confirmation_body); + setCheckboxText(R.string.fxa_signout_confirmation_checkbox); + } + + @Override + public void show(int aShowFlags) { + BitmapDrawable profilePicture = mAccounts.getProfilePicture(); + if (profilePicture != null) { + setIcon(profilePicture); + + } else { + setIcon(R.drawable.ic_icon_settings_account); + } + + super.show(aShowFlags); + } +} 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 8bbd53fd9..52a6d1d42 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 @@ -2,13 +2,16 @@ import android.content.Context; import android.util.AttributeSet; -import android.view.View; import org.mozilla.vrbrowser.ui.widgets.UIWidget; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; -import org.mozilla.vrbrowser.utils.ViewUtils; -public abstract class UIDialog extends UIWidget implements WidgetManagerDelegate.FocusChangeListener { +import java.util.LinkedList; + +public abstract class UIDialog extends UIWidget implements WidgetManagerDelegate.WorldClickListener { + + private static LinkedList mDialogs = new LinkedList<>(); + public UIDialog(Context aContext) { super(aContext); initialize(); @@ -25,12 +28,12 @@ public UIDialog(Context aContext, AttributeSet aAttrs, int aDefStyle) { } private void initialize() { - mWidgetManager.addFocusChangeListener(this); + mWidgetManager.addWorldClickListener(this); } @Override public void releaseWidget() { - mWidgetManager.removeFocusChangeListener(this); + mWidgetManager.removeWorldClickListener(this); super.releaseWidget(); } @@ -39,13 +42,56 @@ public boolean isDialog() { return true; } - // WidgetManagerDelegate.FocusChangeListener + @Override + public void show(int aShowFlags) { + if (!isVisible()) { + super.show(aShowFlags); + + mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); + + UIDialog head = mDialogs.peek(); + if (head != null && head.isVisible()) { + head.hide(); + } + mDialogs.push(this); + } + } + + @Override + public void hide(int aHideFlags) { + super.hide(aHideFlags); + + mWidgetManager.popWorldBrightness(this); + + mDialogs.remove(this); + UIDialog head = mDialogs.peek(); + if (head != null) { + head.show(); + } + } + + private void show() { + if (!isVisible()) { + super.show(REQUEST_FOCUS); + + mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); + } + } + + private void hide() { + super.hide(KEEP_WIDGET); + + mWidgetManager.popWorldBrightness(this); + } @Override - public void onGlobalFocusChanged(View oldFocus, View newFocus) { - if (!ViewUtils.isEqualOrChildrenOf(this, newFocus) && isVisible()) { - onDismiss(); + public void onWorldClick() { + if (isVisible()) { + post(this::onDismiss); } } + public static void closeAllDialogs() { + new LinkedList<>(mDialogs).forEach(dialog -> dialog.onDismiss()); + } } 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 617c53271..af3595d13 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 @@ -19,14 +19,13 @@ import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; -import android.view.View; +import android.view.LayoutInflater; import android.view.animation.Animation; import android.view.animation.LinearInterpolator; import android.view.animation.RotateAnimation; -import android.widget.ImageView; -import android.widget.TextView; import androidx.core.app.ActivityCompat; +import androidx.databinding.DataBindingUtil; import com.mozilla.speechlibrary.ISpeechRecognitionListener; import com.mozilla.speechlibrary.MozillaSpeechService; @@ -39,7 +38,7 @@ import org.mozilla.vrbrowser.audio.AudioEngine; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.browser.engine.SessionStore; -import org.mozilla.vrbrowser.ui.views.UIButton; +import org.mozilla.vrbrowser.databinding.VoiceSearchDialogBinding; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; import org.mozilla.vrbrowser.utils.LocaleUtils; @@ -49,7 +48,14 @@ public class VoiceSearchWidget extends UIDialog implements WidgetManagerDelegate.PermissionListener, Application.ActivityLifecycleCallbacks { - private static final int VOICESEARCH_AUDIO_REQUEST_CODE = 7455; + public enum State { + LISTENING, + SEARCHING, + ERROR, + PERMISSIONS + } + + private static final int VOICE_SEARCH_AUDIO_REQUEST_CODE = 7455; private static final int ANIMATION_DURATION = 1000; private static int MAX_CLIPPING = 10000; @@ -62,16 +68,10 @@ public interface VoiceSearchDelegate { default void OnVoiceSearchError() {}; } + private VoiceSearchDialogBinding mBinding; private MozillaSpeechService mMozillaSpeechService; private VoiceSearchDelegate mDelegate; - private ImageView mVoiceSearchInput; - private ImageView mVoiceSearchSearching; - private Drawable mVoiceInputBackgroundDrawable; private ClipDrawable mVoiceInputClipDrawable; - private int mVoiceInputGravity; - private TextView mVoiceSearchText1; - private TextView mVoiceSearchText2; - private TextView mVoiceSearchText3; private RotateAnimation mSearchingAnimation; private boolean mIsSpeechRecognitionRunning = false; private boolean mWasSpeechRecognitionRunning = false; @@ -94,9 +94,10 @@ public VoiceSearchWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { } private void initialize(Context aContext) { - inflate(aContext, R.layout.voice_search_dialog, this); + LayoutInflater inflater = LayoutInflater.from(aContext); - mAudio = AudioEngine.fromContext(aContext); + // Inflate this data binding layout + mBinding = DataBindingUtil.inflate(inflater, R.layout.voice_search_dialog, this, true); mModelPath = getContext().getExternalFilesDir("models").getAbsolutePath(); @@ -105,17 +106,11 @@ private void initialize(Context aContext) { mMozillaSpeechService = MozillaSpeechService.getInstance(); mMozillaSpeechService.setProductTag(getContext().getString(R.string.voice_app_id)); - mVoiceSearchText1 = findViewById(R.id.voiceSearchText1); - mVoiceSearchText2 = findViewById(R.id.voiceSearchText2); - mVoiceSearchText3 = findViewById(R.id.voiceSearchText3); - - mVoiceInputGravity = 0; - mVoiceInputBackgroundDrawable = getResources().getDrawable(R.drawable.ic_voice_search_volume_input_black, getContext().getTheme()); + Drawable mVoiceInputBackgroundDrawable = getResources().getDrawable(R.drawable.ic_voice_search_volume_input_black, getContext().getTheme()); mVoiceInputClipDrawable = new ClipDrawable(getContext().getDrawable(R.drawable.ic_voice_search_volume_input_clip), Gravity.START, ClipDrawable.HORIZONTAL); Drawable[] layers = new Drawable[] {mVoiceInputBackgroundDrawable, mVoiceInputClipDrawable }; - mVoiceSearchInput = findViewById(R.id.voiceSearchInput); - mVoiceSearchInput.setImageDrawable(new LayerDrawable(layers)); - mVoiceInputClipDrawable.setLevel(mVoiceInputGravity); + mBinding.voiceSearchAnimationListening.setImageDrawable(new LayerDrawable(layers)); + mVoiceInputClipDrawable.setLevel(0); mSearchingAnimation = new RotateAnimation(0, 360f, Animation.RELATIVE_TO_SELF, 0.5f, @@ -124,16 +119,8 @@ private void initialize(Context aContext) { mSearchingAnimation.setInterpolator(new LinearInterpolator()); mSearchingAnimation.setDuration(ANIMATION_DURATION); mSearchingAnimation.setRepeatCount(Animation.INFINITE); - mVoiceSearchSearching = findViewById(R.id.voiceSearchSearching); - UIButton backButton = findViewById(R.id.backButton); - backButton.setOnClickListener(view -> { - if (mAudio != null) { - mAudio.playSound(AudioEngine.Sound.CLICK); - } - - onDismiss(); - }); + mBinding.closeButton.setOnClickListener(view -> onDismiss()); mMozillaSpeechService.addListener(mVoiceSearchListener); ((Application)aContext.getApplicationContext()).registerActivityLifecycleCallbacks(this); @@ -155,13 +142,16 @@ public void releaseWidget() { @Override protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.visible = false; - aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.voice_search_width); - aPlacement.height = WidgetPlacement.dpDimension(getContext(), R.dimen.voice_search_height); - aPlacement.parentAnchorX = 0.5f; - aPlacement.parentAnchorY = 0.5f; + aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.prompt_dialog_width); + aPlacement.height = WidgetPlacement.dpDimension(getContext(), R.dimen.prompt_dialog_height); aPlacement.anchorX = 0.5f; aPlacement.anchorY = 0.5f; - aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.voice_search_world_z); + aPlacement.parentAnchorX = 0.5f; + aPlacement.parentAnchorY = 0.0f; + aPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.settings_world_y) - + WidgetPlacement.unitFromMeters(getContext(), R.dimen.window_world_y); + aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.settings_world_z) - + WidgetPlacement.unitFromMeters(getContext(), R.dimen.window_world_z); } public void setPlacementForKeyboard(int aHandle) { @@ -238,7 +228,7 @@ public void startVoiceSearch() { if (ActivityCompat.checkSelfPermission(getContext().getApplicationContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions((Activity)getContext(), new String[]{Manifest.permission.RECORD_AUDIO}, - VOICESEARCH_AUDIO_REQUEST_CODE); + VOICE_SEARCH_AUDIO_REQUEST_CODE); } else { String locale = LocaleUtils.getVoiceSearchLocale(getContext()); mMozillaSpeechService.setLanguage(LocaleUtils.mapToMozillaSpeechLocales(locale)); @@ -278,7 +268,7 @@ public void stopVoiceSearch() { @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { boolean granted = false; - if (requestCode == VOICESEARCH_AUDIO_REQUEST_CODE) { + if (requestCode == VOICE_SEARCH_AUDIO_REQUEST_CODE) { for (int result: grantResults) { if (result == PackageManager.PERMISSION_GRANTED) { granted = true; @@ -300,6 +290,7 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in public void show(@ShowFlags int aShowFlags) { if (SettingsStore.getInstance(getContext()).isSpeechDataCollectionEnabled() || SettingsStore.getInstance(getContext()).isSpeechDataCollectionReviewed()) { + mWidgetPlacement.parentHandle = mWidgetManager.getFocusedWindow().getHandle(); super.show(aShowFlags); setStartListeningState(); @@ -307,7 +298,7 @@ public void show(@ShowFlags int aShowFlags) { startVoiceSearch(); } else { - mWidgetManager.getFocusedWindow().showAppDialog( + mWidgetManager.getFocusedWindow().showDialog( getResources().getString(R.string.voice_samples_collect_data_dialog_title, getResources().getString(R.string.app_name)), R.string.voice_samples_collect_dialog_description2, new int[]{ @@ -315,14 +306,13 @@ public void show(@ShowFlags int aShowFlags) { R.string.voice_samples_collect_dialog_allow}, index -> { SettingsStore.getInstance(getContext()).setSpeechDataCollectionReviewed(true); - if (index == MessageDialogWidget.POSITIVE) { + if (index == PromptDialogWidget.POSITIVE) { SettingsStore.getInstance(getContext()).setSpeechDataCollectionEnabled(true); } ThreadUtils.postToUiThread(() -> show(aShowFlags)); }, - url -> { - mWidgetManager.getFocusedWindow().getSession().loadUri(getResources().getString(R.string.private_policy_url)); - onDismiss(); + () -> { + mWidgetManager.openNewTabForeground(getResources().getString(R.string.private_policy_url)); }); } } @@ -332,53 +322,37 @@ public void hide(@HideFlags int aHideFlags) { super.hide(aHideFlags); stopVoiceSearch(); + mBinding.setState(State.LISTENING); } private void setStartListeningState() { - mVoiceSearchText1.setText(R.string.voice_search_start); - mVoiceSearchText1.setVisibility(View.VISIBLE); - mVoiceSearchText2.setVisibility(View.GONE); - mVoiceSearchText3.setVisibility(View.VISIBLE); - mVoiceSearchInput.setVisibility(View.VISIBLE); - mVoiceSearchSearching.clearAnimation(); - mVoiceSearchSearching.setVisibility(View.INVISIBLE); + mBinding.setState(State.LISTENING); + mBinding.voiceSearchAnimationSearching.clearAnimation(); + mBinding.executePendingBindings(); } private void setDecodingState() { - mVoiceSearchText1.setText(R.string.voice_search_decoding); - mVoiceSearchText1.setVisibility(View.VISIBLE); - mVoiceSearchText2.setVisibility(View.GONE); - mVoiceSearchText3.setVisibility(View.INVISIBLE); - mVoiceSearchInput.setVisibility(View.INVISIBLE); - mVoiceSearchSearching.startAnimation(mSearchingAnimation); - mVoiceSearchSearching.setVisibility(View.VISIBLE); + mBinding.setState(State.SEARCHING); + mBinding.voiceSearchAnimationSearching.startAnimation(mSearchingAnimation); + mBinding.executePendingBindings(); } private void setResultState() { stopVoiceSearch(); postDelayed(() -> { - mVoiceSearchText1.setText(R.string.voice_search_error); - mVoiceSearchText1.setVisibility(View.VISIBLE); - mVoiceSearchText2.setText(R.string.voice_search_try_again); - mVoiceSearchText2.setVisibility(View.VISIBLE); - mVoiceSearchText3.setVisibility(View.VISIBLE); - mVoiceSearchInput.setVisibility(View.VISIBLE); - mVoiceSearchSearching.clearAnimation(); - mVoiceSearchSearching.setVisibility(View.INVISIBLE); + mBinding.setState(State.ERROR); + mBinding.voiceSearchAnimationSearching.clearAnimation(); + mBinding.executePendingBindings(); startVoiceSearch(); }, 100); } private void setPermissionNotGranted() { - mVoiceSearchText1.setText(R.string.voice_search_permission_after_decline); - mVoiceSearchText1.setVisibility(View.VISIBLE); - mVoiceSearchText2.setVisibility(View.GONE); - mVoiceSearchText3.setVisibility(View.INVISIBLE); - mVoiceSearchInput.setVisibility(View.INVISIBLE); - mVoiceSearchSearching.clearAnimation(); - mVoiceSearchSearching.setVisibility(View.INVISIBLE); + mBinding.setState(State.PERMISSIONS); + mBinding.voiceSearchAnimationSearching.clearAnimation(); + mBinding.executePendingBindings(); } @Override diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/WhatsNewWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/WhatsNewWidget.java index cb87cf123..74f6de45b 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/WhatsNewWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/dialogs/WhatsNewWidget.java @@ -5,87 +5,61 @@ package org.mozilla.vrbrowser.ui.widgets.dialogs; -import android.annotation.SuppressLint; import android.content.Context; import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.databinding.DataBindingUtil; import org.mozilla.geckoview.GeckoSessionSettings; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.VRBrowserApplication; import org.mozilla.vrbrowser.browser.Accounts; import org.mozilla.vrbrowser.browser.SettingsStore; -import org.mozilla.vrbrowser.databinding.WhatsNewBinding; -import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; -import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; +import org.mozilla.vrbrowser.telemetry.GleanMetricsService; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; -public class WhatsNewWidget extends UIDialog implements WidgetManagerDelegate.WorldClickListener { +public class WhatsNewWidget extends PromptDialogWidget { private Accounts mAccounts; - private Runnable mSignInCallback; - private Runnable mStartBrowsingCallback; private Accounts.LoginOrigin mLoginOrigin; private Executor mUIThreadExecutor; public WhatsNewWidget(Context aContext) { super(aContext); - initialize(); - } - - public void setSignInCallback(@NonNull Runnable callback) { - mSignInCallback = callback; - } - public void setStartBrowsingCallback(@NonNull Runnable callback) { - mStartBrowsingCallback = callback; + initialize(aContext); } - @SuppressLint("ClickableViewAccessibility") - private void initialize() { - LayoutInflater inflater = LayoutInflater.from(getContext()); + @Override + protected void initialize(Context aContext) { + super.initialize(aContext); mUIThreadExecutor = ((VRBrowserApplication)getContext().getApplicationContext()).getExecutors().mainThread(); - mAccounts = ((VRBrowserApplication)getContext().getApplicationContext()).getAccounts(); - // Inflate this data binding layout - WhatsNewBinding mBinding = DataBindingUtil.inflate(inflater, R.layout.whats_new, this, true); + setButtons(new int[] { + R.string.whats_new_button_start_browsing, + R.string.whats_new_button_sign_in + }); + setButtonsDelegate(index -> { + if (index == PromptDialogWidget.NEGATIVE) { + onDismiss(); - mBinding.signInButton.setOnClickListener(this::signIn); - mBinding.startBrowsingButton.setOnClickListener(this::startBrowsing); - } + } else if (index == PromptDialogWidget.POSITIVE) { + signIn(); + } + }); - public void setLoginOrigin(Accounts.LoginOrigin origin) { - mLoginOrigin = origin; - } + setCheckboxVisible(false); - @Override - protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { - aPlacement.visible = false; - aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.whats_new_width); - aPlacement.height = WidgetPlacement.dpDimension(getContext(), R.dimen.whats_new_height); - aPlacement.parentAnchorX = 0.5f; - aPlacement.parentAnchorY = 0.0f; - aPlacement.anchorX = 0.5f; - aPlacement.anchorY = 0.5f; - aPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.settings_world_y) - - WidgetPlacement.unitFromMeters(getContext(), R.dimen.window_world_y); - aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.settings_world_z) - - WidgetPlacement.unitFromMeters(getContext(), R.dimen.window_world_z); + setIcon(R.drawable.ic_asset_image_accounts); + setTitle(R.string.whats_new_title_1); + setBody(R.string.whats_new_body_1); + setDescription(R.string.whats_new_body_sub_1); } - @Override - public void show(@ShowFlags int aShowFlags) { - super.show(aShowFlags); - - mWidgetManager.addWorldClickListener(this); - mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); + public void setLoginOrigin(Accounts.LoginOrigin origin) { + mLoginOrigin = origin; } @Override @@ -93,42 +67,36 @@ public void hide(@HideFlags int aHideFlags) { super.hide(aHideFlags); SettingsStore.getInstance(getContext()).setWhatsNewDisplayed(true); - - mWidgetManager.popWorldBrightness(this); - mWidgetManager.removeWorldClickListener(this); } - private void signIn(View view) { - mAccounts.getAuthenticationUrlAsync().thenAcceptAsync((url) -> { - if (url != null) { - mAccounts.setLoginOrigin(mLoginOrigin); - mWidgetManager.openNewTabForeground(url); - mWidgetManager.getFocusedWindow().getSession().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_VR); - mWidgetManager.getFocusedWindow().getSession().loadUri(url); + private void signIn() { + if (mAccounts.getAccountStatus() == Accounts.AccountStatus.SIGNED_IN) { + mAccounts.logoutAsync(); + + } else { + UIDialog.closeAllDialogs(); + + CompletableFuture result = mAccounts.authUrlAsync(); + if (result != null) { + result.thenAcceptAsync((url) -> { + if (url == null) { + mAccounts.logoutAsync(); + + } else { + mAccounts.setLoginOrigin(mLoginOrigin); + mWidgetManager.openNewTabForeground(url); + mWidgetManager.getFocusedWindow().getSession().loadUri(url); + mWidgetManager.getFocusedWindow().getSession().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_VR); + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.FXA_LOGIN); + } + + }, mUIThreadExecutor).exceptionally(throwable -> { + Log.d(LOGTAG, "Error getting the authentication URL: " + throwable.getLocalizedMessage()); + throwable.printStackTrace(); + return null; + }); } - - if (mSignInCallback != null) { - mSignInCallback.run(); - } - - }, mUIThreadExecutor).exceptionally(throwable -> { - Log.d(LOGTAG, "Error getting the authentication URL: " + throwable.getLocalizedMessage()); - throwable.printStackTrace(); - return null; - }); - } - - private void startBrowsing(View view) { - if (mStartBrowsingCallback != null) { - mStartBrowsingCallback.run(); } } - // WidgetManagerDelegate.WorldClickListener - - @Override - public void onWorldClick() { - onDismiss(); - } - } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/ContextMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/ContextMenuWidget.java index 0752ada94..8cb527dfc 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/ContextMenuWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/ContextMenuWidget.java @@ -13,6 +13,7 @@ import org.mozilla.geckoview.GeckoSession; import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; import org.mozilla.vrbrowser.utils.StringUtils; @@ -87,6 +88,7 @@ public void setContextElement(GeckoSession.ContentDelegate.ContextElement aConte mItems.add(new MenuWidget.MenuItem(getContext().getString(R.string.context_menu_open_new_tab_1), 0, () -> { if (!StringUtils.isEmpty(aContextElement.linkUri)) { mWidgetManager.openNewTab(aContextElement.linkUri); + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.CONTEXT_MENU); } onDismiss(); })); @@ -122,7 +124,7 @@ public void setContextElement(GeckoSession.ContentDelegate.ContextElement aConte @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { - if (!ViewUtils.isChildrenOf(this, newFocus) && isVisible()) { + if (!ViewUtils.isEqualOrChildrenOf(this, newFocus) && isVisible()) { onDismiss(); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/HamburgerMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/HamburgerMenuWidget.java index 52c1307b6..416404fe7 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/HamburgerMenuWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/HamburgerMenuWidget.java @@ -123,8 +123,8 @@ private void createMenuItems() { @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { - if (!ViewUtils.isChildrenOf(menuContainer, newFocus)) { - hide(KEEP_WIDGET); + if (!ViewUtils.isEqualOrChildrenOf(this, newFocus)) { + onDismiss(); } } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/LibraryMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/LibraryMenuWidget.java index 7b3275764..b7bd67399 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/LibraryMenuWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/LibraryMenuWidget.java @@ -140,8 +140,8 @@ private void createMenuItems(boolean canOpenWindows, boolean isBookmarked) { @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { - if (!ViewUtils.isChildrenOf(menuContainer, newFocus)) { - hide(KEEP_WIDGET); + if (!ViewUtils.isEqualOrChildrenOf(this, newFocus)) { + onDismiss(); } } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/VideoProjectionMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/VideoProjectionMenuWidget.java index d2ca8cedd..ab2091c99 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/VideoProjectionMenuWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/VideoProjectionMenuWidget.java @@ -2,24 +2,29 @@ import android.content.Context; import android.net.Uri; +import android.view.View; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import org.mozilla.vrbrowser.R; 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; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.IntStream; -public class VideoProjectionMenuWidget extends MenuWidget { +public class VideoProjectionMenuWidget extends MenuWidget implements WidgetManagerDelegate.FocusChangeListener { - @IntDef(value = { VIDEO_PROJECTION_3D_SIDE_BY_SIDE, VIDEO_PROJECTION_360, + @IntDef(value = { VIDEO_PROJECTION_NONE, VIDEO_PROJECTION_3D_SIDE_BY_SIDE, VIDEO_PROJECTION_360, VIDEO_PROJECTION_360_STEREO, VIDEO_PROJECTION_180, VIDEO_PROJECTION_180_STEREO_LEFT_RIGHT, VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM }) public @interface VideoProjectionFlags {} + public static final int VIDEO_PROJECTION_NONE = -1; public static final int VIDEO_PROJECTION_3D_SIDE_BY_SIDE = 0; public static final int VIDEO_PROJECTION_360 = 1; public static final int VIDEO_PROJECTION_360_STEREO = 2; @@ -31,6 +36,14 @@ public interface Delegate { void onVideoProjectionClick(@VideoProjectionFlags int aProjection); } + class ProjectionMenuItem extends MenuItem { + @VideoProjectionFlags int projection; + public ProjectionMenuItem(@VideoProjectionFlags int aProjection, String aString, int aImage) { + super(aString, aImage, () -> handleClick(aProjection)); + projection = aProjection; + } + } + ArrayList mItems; Delegate mDelegate; @VideoProjectionFlags int mSelectedProjection = VIDEO_PROJECTION_3D_SIDE_BY_SIDE; @@ -52,6 +65,20 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.translationZ = 2.0f; } + @Override + public void show(@ShowFlags int aShowFlags) { + super.show(aShowFlags); + + mWidgetManager.addFocusChangeListener(VideoProjectionMenuWidget.this); + } + + @Override + public void hide(@HideFlags int aHideFlags) { + super.hide(aHideFlags); + + mWidgetManager.removeFocusChangeListener(this); + } + public void setParentWidget(UIWidget aParent) { mWidgetPlacement.parentHandle = aParent.getHandle(); } @@ -63,23 +90,23 @@ public void setDelegate(@Nullable Delegate aDelegate) { private void createMenuItems() { mItems = new ArrayList<>(); - mItems.add(new MenuItem(getContext().getString(R.string.video_mode_3d_side), - R.drawable.ic_icon_videoplayback_3dsidebyside, () -> handleClick(VIDEO_PROJECTION_3D_SIDE_BY_SIDE))); + mItems.add(new ProjectionMenuItem(VIDEO_PROJECTION_3D_SIDE_BY_SIDE, getContext().getString(R.string.video_mode_3d_side), + R.drawable.ic_icon_videoplayback_3dsidebyside)); - mItems.add(new MenuItem(getContext().getString(R.string.video_mode_360), - R.drawable.ic_icon_videoplayback_360, () -> handleClick(VIDEO_PROJECTION_360))); + mItems.add(new ProjectionMenuItem(VIDEO_PROJECTION_360, getContext().getString(R.string.video_mode_360), + R.drawable.ic_icon_videoplayback_360)); - mItems.add(new MenuItem(getContext().getString(R.string.video_mode_360_stereo), - R.drawable.ic_icon_videoplayback_360_stereo, () -> handleClick(VIDEO_PROJECTION_360_STEREO))); + mItems.add(new ProjectionMenuItem(VIDEO_PROJECTION_360_STEREO, getContext().getString(R.string.video_mode_360_stereo), + R.drawable.ic_icon_videoplayback_360_stereo)); - mItems.add(new MenuItem(getContext().getString(R.string.video_mode_180), - R.drawable.ic_icon_videoplayback_180, () -> handleClick(VIDEO_PROJECTION_180))); + mItems.add(new ProjectionMenuItem(VIDEO_PROJECTION_180, getContext().getString(R.string.video_mode_180), + R.drawable.ic_icon_videoplayback_180)); - mItems.add(new MenuItem(getContext().getString(R.string.video_mode_180_left_right), - R.drawable.ic_icon_videoplayback_180_stereo_leftright, () -> handleClick(VIDEO_PROJECTION_180_STEREO_LEFT_RIGHT))); + mItems.add(new ProjectionMenuItem(VIDEO_PROJECTION_180_STEREO_LEFT_RIGHT, getContext().getString(R.string.video_mode_180_left_right), + R.drawable.ic_icon_videoplayback_180_stereo_leftright)); - mItems.add(new MenuItem(getContext().getString(R.string.video_mode_180_top_bottom), - R.drawable.ic_icon_videoplayback_180_stereo_topbottom, () -> handleClick(VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM))); + mItems.add(new ProjectionMenuItem(VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM, getContext().getString(R.string.video_mode_180_top_bottom), + R.drawable.ic_icon_videoplayback_180_stereo_topbottom)); super.updateMenuItems(mItems); @@ -101,23 +128,28 @@ private void handleClick(@VideoProjectionFlags int aVideoProjection) { public void setSelectedProjection(@VideoProjectionFlags int aProjection) { mSelectedProjection = aProjection; + int index = IntStream.range(0, mItems.size()) + .filter(i -> ((ProjectionMenuItem)mItems.get(i)).projection == aProjection) + .findFirst() + .orElse(-1); + setSelectedItem(index); } - public static @VideoProjectionFlags Integer getAutomaticProjection(String aURL, AtomicBoolean autoEnter) { + public static @VideoProjectionFlags int getAutomaticProjection(String aURL, AtomicBoolean autoEnter) { if (aURL == null) { - return null; + return VIDEO_PROJECTION_NONE; } Uri uri = Uri.parse(aURL); if (uri == null) { - return null; + return VIDEO_PROJECTION_NONE; } String projection = uri.getQueryParameter("mozVideoProjection"); if (projection == null) { projection = uri.getQueryParameter("mozvideoprojection"); if (projection == null) { - return null; + return VIDEO_PROJECTION_NONE; } } projection = projection.toLowerCase(); @@ -138,6 +170,14 @@ public void setSelectedProjection(@VideoProjectionFlags int aProjection) { return VIDEO_PROJECTION_3D_SIDE_BY_SIDE; } - return -1; + return VIDEO_PROJECTION_NONE; + } + + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + if (!ViewUtils.isEqualOrChildrenOf(this, newFocus) && isVisible()) { + onDismiss(); + } } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/prompts/ChoicePromptWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/prompts/ChoicePromptWidget.java index 6a391c332..8031f3c06 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/prompts/ChoicePromptWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/prompts/ChoicePromptWidget.java @@ -17,7 +17,6 @@ import androidx.annotation.NonNull; -import org.mozilla.geckoview.GeckoSession; import org.mozilla.geckoview.GeckoSession.PromptDelegate.ChoicePrompt.Choice; import org.mozilla.geckoview.GeckoSession.PromptDelegate.ChoicePrompt.Type; import org.mozilla.vrbrowser.R; @@ -61,8 +60,6 @@ public ChoicePromptWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) protected void initialize(Context aContext) { inflate(aContext, R.layout.prompt_choice, this); - mWidgetManager.addFocusChangeListener(this); - mAudio = AudioEngine.fromContext(aContext); mLayout = findViewById(R.id.layout); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/prompts/ConfirmPromptWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/prompts/ConfirmPromptWidget.java index 752624cdf..a814c5cd4 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/prompts/ConfirmPromptWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/prompts/ConfirmPromptWidget.java @@ -40,8 +40,6 @@ public ConfirmPromptWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) protected void initialize(Context aContext) { inflate(aContext, R.layout.prompt_confirm, this); - mWidgetManager.addFocusChangeListener(this); - mAudio = AudioEngine.fromContext(aContext); mLayout = findViewById(R.id.layout); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/prompts/PromptWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/prompts/PromptWidget.java index 62eceb51e..a2b0162fe 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/prompts/PromptWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/prompts/PromptWidget.java @@ -7,13 +7,15 @@ import android.view.ViewTreeObserver; import android.widget.TextView; -import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; import org.mozilla.vrbrowser.ui.widgets.dialogs.UIDialog; +/** + * Base widget used for the browser triggered prompts: alert, confirm, prompt, auth and select + */ public class PromptWidget extends UIDialog { public interface PromptDelegate { @@ -38,7 +40,7 @@ public PromptWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { super(aContext, aAttrs, aDefStyle); } - public void setPromptDelegate(@NonNull PromptDelegate delegate) { + public void setPromptDelegate(@Nullable PromptDelegate delegate) { mPromptDelegate = delegate; } @@ -63,7 +65,7 @@ public void setMessage(String message) { @Override protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.visible = false; - aPlacement.width = WidgetPlacement.pixelDimension(getContext(), R.dimen.browser_width_pixels)/2; + aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.prompt_width); mMaxHeight = WidgetPlacement.dpDimension(getContext(), R.dimen.prompt_height); aPlacement.height = mMaxHeight; aPlacement.parentAnchorX = 0.5f; @@ -73,7 +75,6 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.browser_children_z_distance); } - @Override public void show(@ShowFlags int aShowFlags) { mLayout.measure(View.MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), @@ -83,8 +84,6 @@ public void show(@ShowFlags int aShowFlags) { getMinHeight(); super.show(aShowFlags); - mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); - ViewTreeObserver viewTreeObserver = mLayout.getViewTreeObserver(); if (viewTreeObserver.isAlive()) { viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @@ -98,11 +97,6 @@ public void onGlobalLayout() { } } - public void hide(@HideFlags int aHideFlags) { - super.hide(aHideFlags); - mWidgetManager.popWorldBrightness(this); - } - @Override protected void onDismiss() { hide(REMOVE_WIDGET); @@ -116,14 +110,6 @@ protected void onDismiss() { } } - // WidgetManagerDelegate.FocusChangeListener - @Override - public void onGlobalFocusChanged(View oldFocus, View newFocus) { - if (oldFocus == this && isVisible() && findViewById(newFocus.getId()) == null) { - onDismiss(); - } - } - public int getMinHeight() { return 0; } 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 ceec75edb..a3ac3e156 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 @@ -24,7 +24,8 @@ import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; import org.mozilla.vrbrowser.utils.LocaleUtils; -import java.util.Arrays; +import java.util.Collections; +import java.util.List; public class ContentLanguageOptionsView extends SettingsView { @@ -41,11 +42,11 @@ private void initialize(Context aContext) { LayoutInflater inflater = LayoutInflater.from(aContext); // Preferred languages adapter - mPreferredAdapter = new LanguagesAdapter(mLanguageItemCallback, true); + mPreferredAdapter = new LanguagesAdapter(getContext(), mLanguageItemCallback, true); mPreferredAdapter.setLanguageList(LocaleUtils.getPreferredLanguages(getContext())); // Available languages adapter - mAvailableAdapter = new LanguagesAdapter(mLanguageItemCallback, false); + mAvailableAdapter = new LanguagesAdapter(getContext(), mLanguageItemCallback, false); mAvailableAdapter.setLanguageList(LocaleUtils.getAvailableLanguages()); // Inflate this data binding layout @@ -76,8 +77,8 @@ private void initialize(Context aContext) { @Override public Point getDimensions() { - return new Point( WidgetPlacement.dpDimension(getContext(), R.dimen.language_options_width), - WidgetPlacement.dpDimension(getContext(), R.dimen.language_options_height)); + return new Point( WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_width), + WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_height)); } private LanguageItemCallback mLanguageItemCallback = new LanguageItemCallback() { @@ -115,9 +116,9 @@ public void onMoveDown(View view, Language language) { private void saveCurrentLanguages() { SettingsStore.getInstance(getContext()).setContentLocales( - LocaleUtils.getLanguageIdsFromList(mPreferredAdapter.getItems())); + LocaleUtils.getLocalesFromLanguages(mPreferredAdapter.getItems())); SessionStore.get().setLocales( - LocaleUtils.getLanguageIdsFromList(mPreferredAdapter.getItems())); + LocaleUtils.getLocalesFromLanguages(mPreferredAdapter.getItems())); } private void refreshLanguages() { @@ -129,10 +130,14 @@ private void refreshLanguages() { @Override protected boolean reset() { - SettingsStore.getInstance(getContext()).setContentLocales(Arrays.asList(LocaleUtils.getSystemLocale())); - SessionStore.get().setLocales(Arrays.asList(LocaleUtils.getSystemLocale())); - LocaleUtils.resetLanguages(); - refreshLanguages(); + String systemLocale = LocaleUtils.getClosestAvailableLocale(LocaleUtils.getDeviceLanguage().getId()); + List preferredLanguages = LocaleUtils.getPreferredLanguages(getContext()); + if (preferredLanguages.size() > 1 || !preferredLanguages.get(0).getId().equals(systemLocale)) { + SettingsStore.getInstance(getContext()).setContentLocales(Collections.emptyList()); + SessionStore.get().setLocales(Collections.emptyList()); + LocaleUtils.resetLanguages(); + refreshLanguages(); + } return false; } 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 e7e0aa505..491f23fd8 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 @@ -59,10 +59,13 @@ private void initialize(Context aContext) { // Hide Performance Monitor switch until it can handle multiple windows. mBinding.performanceMonitorSwitch.setVisibility(View.GONE); + mBinding.hardwareAccelerationSwitch.setOnCheckedChangeListener(mUIHardwareAccelerationListener); + setUIHardwareAcceleration(SettingsStore.getInstance(getContext()).isUIHardwareAccelerationEnabled(), false); + if (BuildConfig.DEBUG) { mBinding.debugLoggingSwitch.setVisibility(View.GONE); } else { - setDebugLogging(SettingsStore.getInstance(getContext()).isDebugLogginEnabled(), false); + setDebugLogging(SettingsStore.getInstance(getContext()).isDebugLoggingEnabled(), false); } if (!isServoAvailable()) { @@ -94,6 +97,10 @@ private void initialize(Context aContext) { setDebugLogging(value, doApply); }; + private SwitchSetting.OnCheckedChangeListener mUIHardwareAccelerationListener = (compoundButton, value, doApply) -> { + setUIHardwareAcceleration(value, doApply); + }; + private SwitchSetting.OnCheckedChangeListener mServoListener = (compoundButton, b, doApply) -> { setServo(b, true); }; @@ -123,6 +130,11 @@ private void initialize(Context aContext) { restart = true; } + if (mBinding.hardwareAccelerationSwitch.isChecked() != SettingsStore.UI_HARDWARE_ACCELERATION_DEFAULT) { + setUIHardwareAcceleration(SettingsStore.UI_HARDWARE_ACCELERATION_DEFAULT, true); + restart = true; + } + if (restart) { showRestartDialog(); } @@ -164,6 +176,17 @@ private void setMultiprocess(boolean value, boolean doApply) { } } + private void setUIHardwareAcceleration(boolean value, boolean doApply) { + mBinding.hardwareAccelerationSwitch.setOnCheckedChangeListener(null); + mBinding.hardwareAccelerationSwitch.setValue(value, false); + mBinding.hardwareAccelerationSwitch.setOnCheckedChangeListener(mUIHardwareAccelerationListener); + + if (doApply) { + SettingsStore.getInstance(getContext()).setUIHardwareAccelerationEnabled(value); + showRestartDialog(); + } + } + private void setPerformance(boolean value, boolean doApply) { mBinding.performanceMonitorSwitch.setOnCheckedChangeListener(null); mBinding.performanceMonitorSwitch.setValue(value, false); 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 06032b9c7..d8d6670ec 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 @@ -12,6 +12,7 @@ import androidx.databinding.DataBindingUtil; import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.databinding.OptionsLanguageDisplayBinding; import org.mozilla.vrbrowser.ui.views.settings.RadioGroupSetting; @@ -48,28 +49,32 @@ private void initialize(Context aContext) { // Footer mBinding.footerLayout.setFooterButtonClickListener(mResetListener); - String language = LocaleUtils.getDisplayLocale(getContext()); + mBinding.languageRadio.setOptions(LocaleUtils.getSupportedLocalizedLanguages(getContext())); + + String locale = LocaleUtils.getDisplayLocale(getContext()); mBinding.languageRadio.setOnCheckedChangeListener(mLanguageListener); - setLanguage(mBinding.languageRadio.getIdForValue(language), false); + setLanguage(LocaleUtils.getIndexForSupportedLocale(locale), false); } @Override protected boolean reset() { - String systemLocale = LocaleUtils.getSystemLocale(); + String systemLocale = LocaleUtils.getClosestSupportedLocale(getContext(), LocaleUtils.getDeviceLanguage().getId()); String currentLocale = LocaleUtils.getCurrentLocale(); - if (!currentLocale.equalsIgnoreCase(systemLocale)) { - setLanguage(mBinding.languageRadio.getIdForValue(systemLocale), true); - return true; + if (currentLocale.equalsIgnoreCase(systemLocale)) { + setLanguage(LocaleUtils.getIndexForSupportedLocale(systemLocale), false); + SettingsStore.getInstance(getContext()).setDisplayLocale(currentLocale); + return false; } else { - setLanguage(mBinding.languageRadio.getIdForValue(systemLocale), false); - return false; + setLanguage(LocaleUtils.getIndexForSupportedLocale(systemLocale), true); + SettingsStore.getInstance(getContext()).setDisplayLocale(null); + return true; } } private RadioGroupSetting.OnCheckedChangeListener mLanguageListener = (radioGroup, checkedId, doApply) -> { String currentLocale = LocaleUtils.getCurrentLocale(); - String locale = mBinding.languageRadio.getValueForId(mBinding.languageRadio.getCheckedRadioButtonId()).toString(); + String locale = LocaleUtils.getSupportedLocaleForIndex(mBinding.languageRadio.getCheckedRadioButtonId()); if (!locale.equalsIgnoreCase(currentLocale)) { setLanguage(checkedId, true); @@ -86,8 +91,8 @@ private void setLanguage(int checkedId, boolean doApply) { mBinding.languageRadio.setOnCheckedChangeListener(mLanguageListener); if (doApply) { - String language = mBinding.languageRadio.getValueForId(checkedId).toString(); - LocaleUtils.setDisplayLocale(getContext(), language); + String locale = LocaleUtils.getSupportedLocaleForIndex(checkedId); + LocaleUtils.setDisplayLocale(getContext(), locale); if (mDelegate != null) { mDelegate.showRestartDialog(); @@ -97,7 +102,7 @@ private void setLanguage(int checkedId, boolean doApply) { @Override public Point getDimensions() { - return new Point( WidgetPlacement.dpDimension(getContext(), R.dimen.language_options_width), - WidgetPlacement.dpDimension(getContext(), R.dimen.language_options_height)); + return new Point( WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_width), + WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_height)); } } 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 08a02b8e3..38476e98e 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 @@ -59,7 +59,7 @@ private void initialize(Context aContext) { setMSAAMode(mBinding.msaaRadio.getIdForValue(msaaLevel), false); mBinding.autoplaySwitch.setOnCheckedChangeListener(mAutoplayListener); - setAutoplay(SessionStore.get().getAutoplayEnabled(), false); + setAutoplay(SettingsStore.getInstance(getContext()).isAutoplayEnabled(), false); mDefaultHomepageUrl = getContext().getString(R.string.homepage_url); @@ -232,7 +232,6 @@ private void setAutoplay(boolean value, boolean doApply) { mBinding.autoplaySwitch.setOnCheckedChangeListener(mAutoplayListener); if (doApply) { - SessionStore.get().setAutoplayEnabled(value); SettingsStore.getInstance(getContext()).setAutoplayEnabled(value); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/FxAAccountOptionsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/FxAAccountOptionsView.java index 0f09b662b..1278d4b5a 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/FxAAccountOptionsView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/FxAAccountOptionsView.java @@ -10,17 +10,20 @@ import android.view.LayoutInflater; import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.databinding.DataBindingUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.VRBrowserApplication; import org.mozilla.vrbrowser.browser.Accounts; +import org.mozilla.vrbrowser.browser.Places; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.databinding.OptionsFxaAccountBinding; import org.mozilla.vrbrowser.ui.views.settings.SwitchSetting; +import org.mozilla.vrbrowser.ui.widgets.UIWidget; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; +import org.mozilla.vrbrowser.ui.widgets.dialogs.SignOutDialogWidget; import org.mozilla.vrbrowser.utils.SystemUtils; import java.util.Objects; @@ -40,9 +43,9 @@ class FxAAccountOptionsView extends SettingsView { private OptionsFxaAccountBinding mBinding; private Accounts mAccounts; + private Places mPlaces; private Executor mUIThreadExecutor; - private boolean mInitialBookmarksState; - private boolean mInitialHistoryState; + private SignOutDialogWidget mSignOutDialog; public FxAAccountOptionsView(Context aContext, WidgetManagerDelegate aWidgetManager) { super(aContext, aWidgetManager); @@ -61,10 +64,11 @@ private void initialize(Context aContext) { mBinding.headerLayout.setBackClickListener(view -> onDismiss()); mAccounts = ((VRBrowserApplication)getContext().getApplicationContext()).getAccounts(); + mPlaces = ((VRBrowserApplication)getContext().getApplicationContext()).getPlaces(); mUIThreadExecutor = ((VRBrowserApplication)getContext().getApplicationContext()).getExecutors().mainThread(); - mBinding.signButton.setOnClickListener(view -> mAccounts.logoutAsync()); + mBinding.signButton.setOnClickListener(this::signOut); mBinding.syncButton.setOnClickListener(this::sync); mBinding.setIsSyncing(mAccounts.isSyncing()); @@ -86,11 +90,8 @@ public void onShown() { mAccounts.addAccountListener(mAccountListener); mAccounts.addSyncListener(mSyncListener); - mInitialBookmarksState = mAccounts.isEngineEnabled(SyncEngine.Bookmarks.INSTANCE); - mInitialHistoryState = mAccounts.isEngineEnabled(SyncEngine.History.INSTANCE); - - mBinding.bookmarksSyncSwitch.setValue(mInitialBookmarksState, false); - mBinding.historySyncSwitch.setValue(mInitialHistoryState, false); + mBinding.bookmarksSyncSwitch.setValue(mAccounts.isEngineEnabled(SyncEngine.Bookmarks.INSTANCE), false); + mBinding.historySyncSwitch.setValue(mAccounts.isEngineEnabled(SyncEngine.History.INSTANCE), false); updateSyncBindings(mAccounts.isSyncing()); } @@ -101,20 +102,16 @@ public void onHidden() { mAccounts.removeAccountListener(mAccountListener); mAccounts.removeSyncListener(mSyncListener); - - // We only sync engines in case the user has changed the sync engines since previous sync - if (mBinding.bookmarksSyncSwitch.isChecked() != mInitialBookmarksState || - mBinding.historySyncSwitch.isChecked() != mInitialHistoryState) { - mAccounts.syncNowAsync(SyncReason.EngineChange.INSTANCE, false); - } } private SwitchSetting.OnCheckedChangeListener mBookmarksSyncListener = (compoundButton, value, apply) -> { mAccounts.setSyncStatus(SyncEngine.Bookmarks.INSTANCE, value); + mAccounts.syncNowAsync(SyncReason.EngineChange.INSTANCE, false); }; private SwitchSetting.OnCheckedChangeListener mHistorySyncListener = (compoundButton, value, apply) -> { mAccounts.setSyncStatus(SyncEngine.History.INSTANCE, value); + mAccounts.syncNowAsync(SyncReason.EngineChange.INSTANCE, false); }; private void resetOptions() { @@ -132,6 +129,9 @@ public void onStarted() { public void onIdle() { updateSyncBindings(false); + mBinding.bookmarksSyncSwitch.setValue(mAccounts.isEngineEnabled(SyncEngine.Bookmarks.INSTANCE), false); + mBinding.historySyncSwitch.setValue(mAccounts.isEngineEnabled(SyncEngine.History.INSTANCE), false); + // This shouldn't be necessary but for some reason the buttons stays hovered after the sync. // I guess Android restoring it to the latest state (hovered) before being disabled // Probably an Android bindings bug. @@ -165,13 +165,17 @@ void updateCurrentAccountState() { updateProfile(profile); } else { - Objects.requireNonNull(mAccounts.updateProfileAsync()). - thenAcceptAsync((u) -> updateProfile(mAccounts.accountProfile()), mUIThreadExecutor). - exceptionally(throwable -> { - Log.d(LOGTAG, "Error getting the account profile: " + throwable.getLocalizedMessage()); - throwable.printStackTrace(); - return null; - }); + try { + Objects.requireNonNull(mAccounts.updateProfileAsync()). + thenAcceptAsync((u) -> updateProfile(mAccounts.accountProfile()), mUIThreadExecutor). + exceptionally(throwable -> { + Log.d(LOGTAG, "Error getting the account profile: " + throwable.getLocalizedMessage()); + return null; + }); + + } catch (NullPointerException e) { + Log.d(LOGTAG, "Error getting the account profile: " + e.getLocalizedMessage()); + } } break; @@ -191,24 +195,30 @@ private void updateProfile(Profile profile) { } private void sync(View view) { - if (mBinding.bookmarksSyncSwitch.isChecked() != mInitialBookmarksState || - mBinding.historySyncSwitch.isChecked() != mInitialHistoryState) { - mAccounts.syncNowAsync(SyncReason.EngineChange.INSTANCE, false); - } else { - mAccounts.syncNowAsync(SyncReason.User.INSTANCE, false); + mAccounts.syncNowAsync(SyncReason.User.INSTANCE, false); + mAccounts.updateProfileAsync(); + } + + private void signOut(View view) { + if (mSignOutDialog == null) { + mSignOutDialog = new SignOutDialogWidget(getContext()); } + + exitWholeSettings(); + mSignOutDialog.show(UIWidget.REQUEST_FOCUS); } private AccountObserver mAccountListener = new AccountObserver() { @Override - public void onAuthenticated(@NotNull OAuthAccount oAuthAccount, @NotNull AuthType authType) { + public void onAuthenticated(@NonNull OAuthAccount oAuthAccount, @NonNull AuthType authType) { mBinding.signButton.setButtonText(R.string.settings_fxa_account_sign_out); } @Override - public void onProfileUpdated(@NotNull Profile profile) { - post(() -> mBinding.accountEmail.setText(profile.getEmail())); + public void onProfileUpdated(@NonNull Profile profile) { + mBinding.accountEmail.setText(profile.getEmail()); + mBinding.setLastSync(mAccounts.lastSync()); } @Override diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/LanguageOptionsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/LanguageOptionsView.java index a3c2c1db2..8e8ada86d 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/LanguageOptionsView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/LanguageOptionsView.java @@ -92,7 +92,7 @@ protected void onDismiss() { }; private void setVoiceLanguage() { - mBinding.voiceSearchLanguageDescription.setText(getSpannedLanguageText(LocaleUtils.getVoiceSearchLanguageString(getContext())), TextView.BufferType.SPANNABLE); + mBinding.voiceSearchLanguageDescription.setText(getSpannedLanguageText(LocaleUtils.getVoiceSearchLanguage(getContext()).getName()), TextView.BufferType.SPANNABLE); } private void setContentLanguage() { @@ -105,7 +105,7 @@ private void setContentLanguage() { } private void setDisplayLanguage() { - mBinding.displayLanguageDescription.setText(getSpannedLanguageText(LocaleUtils.getDisplayCurrentLanguageString())); + mBinding.displayLanguageDescription.setText(getSpannedLanguageText(LocaleUtils.getDisplayLanguage(getContext()).getName())); } private int getLanguageIndex(@NonNull String text) { @@ -149,8 +149,8 @@ private SpannableStringBuilder getSpannedLanguageText(@NonNull String language) @Override public Point getDimensions() { - return new Point( WidgetPlacement.dpDimension(getContext(), R.dimen.language_options_width), - WidgetPlacement.dpDimension(getContext(), R.dimen.language_options_height)); + return new Point( WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_width), + WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_height)); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/AllowedPopUpsOptionsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/PopUpExceptionsOptionsView.java similarity index 90% rename from app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/AllowedPopUpsOptionsView.java rename to app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/PopUpExceptionsOptionsView.java index 4b6a91a42..f40d83c2b 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/AllowedPopUpsOptionsView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/PopUpExceptionsOptionsView.java @@ -25,13 +25,13 @@ import java.util.List; -class AllowedPopUpsOptionsView extends SettingsView { +class PopUpExceptionsOptionsView extends SettingsView { private OptionsPrivacyPopupsBinding mBinding; private PopUpAdapter mAdapter; private PopUpsViewModel mViewModel; - public AllowedPopUpsOptionsView(Context aContext, WidgetManagerDelegate aWidgetManager) { + public PopUpExceptionsOptionsView(Context aContext, WidgetManagerDelegate aWidgetManager) { super(aContext, aWidgetManager); initialize(aContext); } @@ -68,8 +68,8 @@ private void initialize(Context aContext) { @Override public Point getDimensions() { - return new Point( WidgetPlacement.dpDimension(getContext(), R.dimen.language_options_width), - WidgetPlacement.dpDimension(getContext(), R.dimen.language_options_height)); + return new Point( WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_width), + WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_height)); } @Override @@ -99,6 +99,10 @@ public void onHidden() { public void onChanged(List popUpSites) { if (popUpSites != null) { mAdapter.setSites(popUpSites); + mBinding.setIsEmpty(popUpSites.isEmpty()); + + } else { + mBinding.setIsEmpty(true); } } }; 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 5bff7bd23..8bfca8f07 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 @@ -78,7 +78,7 @@ private void initialize(Context aContext) { TextView permissionsTitleText = findViewById(R.id.permissionsTitle); permissionsTitleText.setText(getContext().getString(R.string.security_options_permissions_title, getContext().getString(R.string.app_name))); - mPopUpsBlockingExceptions = new AllowedPopUpsOptionsView(getContext(), mWidgetManager); + mPopUpsBlockingExceptions = new PopUpExceptionsOptionsView(getContext(), mWidgetManager); mPermissionButtons = new ArrayList<>(); mPermissionButtons.add(Pair.create(findViewById(R.id.cameraPermissionSwitch), Manifest.permission.CAMERA)); @@ -122,6 +122,9 @@ private void initialize(Context aContext) { setPopUpsBlocking(SettingsStore.getInstance(getContext()).isPopUpsBlockingEnabled(), false); mBinding.popUpsBlockingExceptionsButton.setOnClickListener(v -> mDelegate.showView(mPopUpsBlockingExceptions)); + + mBinding.restoreTabsSwitch.setOnCheckedChangeListener(mRestoreTabsListener); + setRestoreTabs(SettingsStore.getInstance(getContext()).isRestoreTabsEnabled(), false); } private void togglePermission(SwitchSetting aButton, String aPermission) { @@ -171,6 +174,10 @@ public void reject() { setPopUpsBlocking(value, doApply); }; + private SwitchSetting.OnCheckedChangeListener mRestoreTabsListener = (compoundButton, value, doApply) -> { + setRestoreTabs(value, doApply); + }; + private void resetOptions() { if (mBinding.drmContentPlaybackSwitch.isChecked() != SettingsStore.DRM_PLAYBACK_DEFAULT) { setDrmContent(SettingsStore.DRM_PLAYBACK_DEFAULT, true); @@ -199,6 +206,10 @@ private void resetOptions() { if (mBinding.popUpsBlockingSwitch.isChecked() != SettingsStore.POP_UPS_BLOCKING_DEFAULT) { setPopUpsBlocking(SettingsStore.POP_UPS_BLOCKING_DEFAULT, true); } + + if (mBinding.restoreTabsSwitch.isChecked() != SettingsStore.RESTORE_TABS_ENABLED) { + setRestoreTabs(SettingsStore.RESTORE_TABS_ENABLED, true); + } } private void setDrmContent(boolean value, boolean doApply) { @@ -273,6 +284,16 @@ private void setPopUpsBlocking(boolean value, boolean doApply) { } } + private void setRestoreTabs(boolean value, boolean doApply) { + mBinding.restoreTabsSwitch.setOnCheckedChangeListener(null); + mBinding.restoreTabsSwitch.setValue(value, false); + mBinding.restoreTabsSwitch.setOnCheckedChangeListener(mRestoreTabsListener); + + if (doApply) { + SettingsStore.getInstance(getContext()).setRestoreTabsEnabled(value); + } + } + @Override public Point getDimensions() { return new Point( WidgetPlacement.dpDimension(getContext(), R.dimen.privacy_options_width), 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 83922bc8d..548c9e555 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 @@ -10,7 +10,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Point; -import android.graphics.drawable.Drawable; +import android.graphics.drawable.BitmapDrawable; import android.text.Html; import android.util.AttributeSet; import android.util.Log; @@ -24,8 +24,6 @@ import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; -import org.jetbrains.annotations.NotNull; -import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.geckoview.GeckoSessionSettings; import org.mozilla.vrbrowser.BuildConfig; import org.mozilla.vrbrowser.R; @@ -36,18 +34,17 @@ import org.mozilla.vrbrowser.browser.engine.Session; import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.databinding.SettingsBinding; +import org.mozilla.vrbrowser.telemetry.GleanMetricsService; 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.ui.widgets.dialogs.RestartDialogWidget; import org.mozilla.vrbrowser.ui.widgets.dialogs.UIDialog; -import org.mozilla.vrbrowser.ui.widgets.prompts.AlertPromptWidget; import org.mozilla.vrbrowser.utils.StringUtils; -import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.URL; import java.net.URLEncoder; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import mozilla.components.concept.sync.AccountObserver; @@ -55,7 +52,7 @@ import mozilla.components.concept.sync.OAuthAccount; import mozilla.components.concept.sync.Profile; -public class SettingsWidget extends UIDialog implements WidgetManagerDelegate.WorldClickListener, SettingsView.Delegate { +public class SettingsWidget extends UIDialog implements SettingsView.Delegate { public enum SettingDialog { MAIN, LANGUAGE, DISPLAY, PRIVACY, DEVELOPER, FXA, ENVIRONMENT, CONTROLLER @@ -66,8 +63,7 @@ public enum SettingDialog { private SettingsView mCurrentView; private int mViewMarginH; private int mViewMarginV; - private int mRestartDialogHandle = -1; - private int mAlertDialogHandle = -1; + private RestartDialogWidget mRestartDialog; private Accounts mAccounts; private Executor mUIThreadExecutor; @@ -107,8 +103,6 @@ private void initialize() { // Inflate this data binding layout mBinding = DataBindingUtil.inflate(inflater, R.layout.settings, this, true); - mWidgetManager.addWorldClickListener(this); - mAccounts = ((VRBrowserApplication)getContext().getApplicationContext()).getAccounts(); mAccounts.addAccountListener(mAccountObserver); @@ -183,7 +177,7 @@ private void initialize() { }); mBinding.surveyLink.setOnClickListener(v -> { - mWidgetManager.getFocusedWindow().getSession().loadUri(getResources().getString(R.string.survey_link)); + mWidgetManager.openNewTabForeground(getResources().getString(R.string.survey_link)); exitWholeSettings(); }); @@ -191,7 +185,7 @@ private void initialize() { if (mAudio != null) { mAudio.playSound(AudioEngine.Sound.CLICK); } - SessionStore.get().getActiveSession().loadUri(getContext().getString(R.string.help_url)); + mWidgetManager.openNewTabForeground(getContext().getString(R.string.help_url)); onDismiss(); }); @@ -225,7 +219,6 @@ private void initialize() { @Override public void releaseWidget() { - mWidgetManager.removeWorldClickListener(this); mAccounts.removeAccountListener(mAccountObserver); super.releaseWidget(); @@ -271,7 +264,7 @@ private void onSettingsReportClick() { Log.e(LOGTAG, "Cannot encode URL"); } - session.loadUri(getContext().getString(R.string.private_report_url, url)); + mWidgetManager.openNewTabForeground(getContext().getString(R.string.private_report_url, url)); onDismiss(); } @@ -280,19 +273,33 @@ private void manageAccount() { switch(mAccounts.getAccountStatus()) { case SIGNED_OUT: case NEEDS_RECONNECT: - hide(REMOVE_WIDGET); - mAccounts.getAuthenticationUrlAsync().thenAcceptAsync((url) -> { - if (url != null) { - mAccounts.setLoginOrigin(Accounts.LoginOrigin.SETTINGS); - WidgetManagerDelegate widgetManager = ((VRBrowserActivity)getContext()); - widgetManager.openNewTabForeground(url); - widgetManager.getFocusedWindow().getSession().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_MOBILE); + if (mAccounts.getAccountStatus() == Accounts.AccountStatus.SIGNED_IN) { + mAccounts.logoutAsync(); + + } else { + hide(REMOVE_WIDGET); + + CompletableFuture result = mAccounts.authUrlAsync(); + if (result != null) { + result.thenAcceptAsync((url) -> { + if (url == null) { + mAccounts.logoutAsync(); + + } else { + mAccounts.setLoginOrigin(Accounts.LoginOrigin.SETTINGS); + mWidgetManager.openNewTabForeground(url); + WidgetManagerDelegate widgetManager = ((VRBrowserActivity)getContext()); + widgetManager.getFocusedWindow().getSession().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_MOBILE); + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.FXA_LOGIN); + } + + }, mUIThreadExecutor).exceptionally(throwable -> { + Log.d(LOGTAG, "Error getting the authentication URL: " + throwable.getLocalizedMessage()); + throwable.printStackTrace(); + return null; + }); } - }, mUIThreadExecutor).exceptionally(throwable -> { - Log.d(LOGTAG, "Error getting the authentication URL: " + throwable.getLocalizedMessage()); - throwable.printStackTrace(); - return null; - }); + } break; case SIGNED_IN: @@ -322,12 +329,12 @@ private void updateCurrentAccountState() { private AccountObserver mAccountObserver = new AccountObserver() { @Override - public void onAuthenticated(@NotNull OAuthAccount oAuthAccount, @NotNull AuthType authType) { + public void onAuthenticated(@NonNull OAuthAccount oAuthAccount, @NonNull AuthType authType) { } @Override - public void onProfileUpdated(@NotNull Profile profile) { + public void onProfileUpdated(@NonNull Profile profile) { updateProfile(profile); } @@ -343,18 +350,9 @@ public void onAuthenticationProblems() { }; private void updateProfile(Profile profile) { - if (profile != null) { - ThreadUtils.postToBackgroundThread(() -> { - try { - URL url = new URL(profile.getAvatar().getUrl()); - Drawable picture = Drawable.createFromStream(url.openStream(), "src"); - post(() -> mBinding.fxaButton.setImageDrawable(picture)); - - } catch (IOException e) { - e.printStackTrace(); - - } - }); + BitmapDrawable profilePicture = mAccounts.getProfilePicture(); + if (profile != null && profilePicture != null) { + mBinding.fxaButton.setImageDrawable(profilePicture); } else { mBinding.fxaButton.setImageDrawable(getContext().getDrawable(R.drawable.ic_icon_settings_account)); @@ -430,18 +428,10 @@ private void showFXAOptionsDialog() { showView(new FxAAccountOptionsView(getContext(), mWidgetManager)); } - // WindowManagerDelegate.FocusChangeListener - @Override - public void onGlobalFocusChanged(View oldFocus, View newFocus) { - if (mCurrentView != null) { - mCurrentView.onGlobalFocusChanged(oldFocus, newFocus); - } else if (oldFocus == this && isVisible()) { - onDismiss(); - } - } - public void show(@ShowFlags int aShowFlags, @NonNull SettingDialog settingDialog) { - show(aShowFlags); + if (!isVisible()) { + show(aShowFlags); + } switch (settingDialog) { case LANGUAGE: @@ -472,24 +462,9 @@ public void show(@ShowFlags int aShowFlags, @NonNull SettingDialog settingDialog public void show(@ShowFlags int aShowFlags) { super.show(aShowFlags); - mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); - updateCurrentAccountState(); } - @Override - public void hide(@HideFlags int aHideFlags) { - super.hide(aHideFlags); - - mWidgetManager.popWorldBrightness(this); - } - - // WidgetManagerDelegate.WorldClickListener - @Override - public void onWorldClick() { - onDismiss(); - } - // SettingsView.Delegate @Override public void onDismiss() { @@ -518,34 +493,16 @@ public void exitWholeSettings() { @Override public void showRestartDialog() { - hide(UIWidget.REMOVE_WIDGET); - - UIWidget widget = getChild(mRestartDialogHandle); - if (widget == null) { - widget = createChild(RestartDialogWidget.class, false); - mRestartDialogHandle = widget.getHandle(); - widget.setDelegate(() -> show(REQUEST_FOCUS)); + if (mRestartDialog == null) { + mRestartDialog = new RestartDialogWidget(getContext()); } - widget.show(REQUEST_FOCUS); + mRestartDialog.show(REQUEST_FOCUS); } @Override public void showAlert(String aTitle, String aMessage) { - hide(UIWidget.KEEP_WIDGET); - - AlertPromptWidget widget = getChild(mAlertDialogHandle); - if (widget == null) { - widget = createChild(AlertPromptWidget.class, false); - mAlertDialogHandle = widget.getHandle(); - widget.setDelegate(() -> show(REQUEST_FOCUS)); - } - widget.getPlacement().translationZ = 0; - widget.getPlacement().parentHandle = mHandle; - widget.setTitle(aTitle); - widget.setMessage(aMessage); - - widget.show(REQUEST_FOCUS); + mWidgetManager.getFocusedWindow().showAlert(aTitle, aMessage, null); } private boolean isLanguagesSubView(View view) { @@ -559,7 +516,7 @@ private boolean isLanguagesSubView(View view) { } private boolean isPrivacySubView(View view) { - if (view instanceof AllowedPopUpsOptionsView) { + if (view instanceof PopUpExceptionsOptionsView) { return true; } 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 748e308c2..52ab1542c 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 @@ -12,6 +12,7 @@ import androidx.databinding.DataBindingUtil; import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.databinding.OptionsLanguageVoiceBinding; import org.mozilla.vrbrowser.ui.views.settings.RadioGroupSetting; @@ -48,23 +49,35 @@ private void initialize(Context aContext) { // Footer mBinding.footerLayout.setFooterButtonClickListener(mResetListener); - String language = LocaleUtils.getVoiceSearchLocale(getContext()); + mBinding.languageRadio.setOptions(LocaleUtils.getSupportedLocalizedLanguages(getContext())); + + String locale = LocaleUtils.getVoiceSearchLocale(getContext()); mBinding.languageRadio.setOnCheckedChangeListener(mLanguageListener); - setLanguage(mBinding.languageRadio.getIdForValue(language), false); + setLanguage(LocaleUtils.getIndexForSupportedLocale(locale), false); } @Override protected boolean reset() { - String value = mBinding.languageRadio.getValueForId(mBinding.languageRadio.getCheckedRadioButtonId()).toString(); - if (!value.equals(LocaleUtils.getSystemLocale())) { - setLanguage(mBinding.languageRadio.getIdForValue(LocaleUtils.getSystemLocale()), true); + String systemLocale = LocaleUtils.getClosestSupportedLocale(getContext(), LocaleUtils.getDeviceLanguage().getId()); + String currentLocale = LocaleUtils.getVoiceSearchLanguage(getContext()).getId(); + if (currentLocale.equalsIgnoreCase(systemLocale)) { + setLanguage(LocaleUtils.getIndexForSupportedLocale(systemLocale), false); + + } else { + setLanguage(LocaleUtils.getIndexForSupportedLocale(systemLocale), true); + SettingsStore.getInstance(getContext()).setVoiceSearchLocale(null); } return false; } private RadioGroupSetting.OnCheckedChangeListener mLanguageListener = (radioGroup, checkedId, doApply) -> { - setLanguage(checkedId, true); + String currentLocale = LocaleUtils.getCurrentLocale(); + String locale = LocaleUtils.getSupportedLocaleForIndex(mBinding.languageRadio.getCheckedRadioButtonId()); + + if (!locale.equalsIgnoreCase(currentLocale)) { + setLanguage(checkedId, true); + } }; private OnClickListener mResetListener = (view) -> { @@ -76,12 +89,15 @@ private void setLanguage(int checkedId, boolean doApply) { mBinding.languageRadio.setChecked(checkedId, doApply); mBinding.languageRadio.setOnCheckedChangeListener(mLanguageListener); - LocaleUtils.setVoiceSearchLocale(getContext(), mBinding.languageRadio.getValueForId(checkedId).toString()); + if (doApply) { + String locale = LocaleUtils.getSupportedLocaleForIndex(checkedId); + LocaleUtils.setVoiceSearchLocale(getContext(), locale); + } } @Override public Point getDimensions() { - return new Point( WidgetPlacement.dpDimension(getContext(), R.dimen.language_options_width), - WidgetPlacement.dpDimension(getContext(), R.dimen.language_options_height)); + return new Point( WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_width), + WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_height)); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/utils/InternalPages.java b/app/src/common/shared/org/mozilla/vrbrowser/utils/InternalPages.java index 11612d9fd..1446f36d1 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/InternalPages.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/InternalPages.java @@ -16,110 +16,84 @@ public class InternalPages { - private static ErrorType fromGeckoErrorToErrorType(int category, int error) { - switch(category) { - case WebRequestError.ERROR_CATEGORY_UNKNOWN: { - return ErrorType.UNKNOWN; + private static ErrorType fromGeckoErrorToErrorType(int error) { + switch(error) { + case WebRequestError.ERROR_SECURITY_SSL: { + return ErrorType.ERROR_SECURITY_SSL; + } + case WebRequestError.ERROR_SECURITY_BAD_CERT: { + return ErrorType.ERROR_SECURITY_BAD_CERT; + } + case WebRequestError.ERROR_NET_INTERRUPT: { + return ErrorType.ERROR_NET_INTERRUPT; + } + case WebRequestError.ERROR_NET_TIMEOUT: { + return ErrorType.ERROR_NET_TIMEOUT; + } + case WebRequestError.ERROR_CONNECTION_REFUSED: { + return ErrorType.ERROR_CONNECTION_REFUSED; + } + case WebRequestError.ERROR_UNKNOWN_SOCKET_TYPE: { + return ErrorType.ERROR_UNKNOWN_SOCKET_TYPE; + } + case WebRequestError.ERROR_REDIRECT_LOOP: { + return ErrorType.ERROR_REDIRECT_LOOP; + } + case WebRequestError.ERROR_OFFLINE: { + return ErrorType.ERROR_OFFLINE; + } + case WebRequestError.ERROR_PORT_BLOCKED: { + return ErrorType.ERROR_PORT_BLOCKED; + } + case WebRequestError.ERROR_NET_RESET: { + return ErrorType.ERROR_NET_RESET; + } + case WebRequestError.ERROR_UNSAFE_CONTENT_TYPE: { + return ErrorType.ERROR_UNSAFE_CONTENT_TYPE; + } + case WebRequestError.ERROR_CORRUPTED_CONTENT: { + return ErrorType.ERROR_CORRUPTED_CONTENT; + } + case WebRequestError.ERROR_CONTENT_CRASHED: { + return ErrorType.ERROR_CONTENT_CRASHED; + } + case WebRequestError.ERROR_INVALID_CONTENT_ENCODING: { + return ErrorType.ERROR_INVALID_CONTENT_ENCODING; } - case WebRequestError.ERROR_CATEGORY_SECURITY: { - switch (error) { - case WebRequestError.ERROR_SECURITY_SSL: { - return ErrorType.ERROR_SECURITY_SSL; - } - case WebRequestError.ERROR_SECURITY_BAD_CERT: { - return ErrorType.ERROR_SECURITY_BAD_CERT; - } - } - } - case WebRequestError.ERROR_CATEGORY_NETWORK: { - switch (error) { - case WebRequestError.ERROR_NET_INTERRUPT: { - return ErrorType.ERROR_NET_INTERRUPT; - } - case WebRequestError.ERROR_NET_TIMEOUT: { - return ErrorType.ERROR_NET_TIMEOUT; - } - case WebRequestError.ERROR_CONNECTION_REFUSED: { - return ErrorType.ERROR_CONNECTION_REFUSED; - } - case WebRequestError.ERROR_UNKNOWN_SOCKET_TYPE: { - return ErrorType.ERROR_UNKNOWN_SOCKET_TYPE; - } - case WebRequestError.ERROR_REDIRECT_LOOP: { - return ErrorType.ERROR_REDIRECT_LOOP; - } - case WebRequestError.ERROR_OFFLINE: { - return ErrorType.ERROR_OFFLINE; - } - case WebRequestError.ERROR_PORT_BLOCKED: { - return ErrorType.ERROR_PORT_BLOCKED; - } - case WebRequestError.ERROR_NET_RESET: { - return ErrorType.ERROR_NET_RESET; - } - } - } - case WebRequestError.ERROR_CATEGORY_CONTENT: { - switch (error) { - case WebRequestError.ERROR_UNSAFE_CONTENT_TYPE: { - return ErrorType.ERROR_UNSAFE_CONTENT_TYPE; - } - case WebRequestError.ERROR_CORRUPTED_CONTENT: { - return ErrorType.ERROR_CORRUPTED_CONTENT; - } - case WebRequestError.ERROR_CONTENT_CRASHED: { - return ErrorType.ERROR_CONTENT_CRASHED; - } - case WebRequestError.ERROR_INVALID_CONTENT_ENCODING: { - return ErrorType.ERROR_INVALID_CONTENT_ENCODING; - } - } - } - case WebRequestError.ERROR_CATEGORY_URI: { - switch (error) { - case WebRequestError.ERROR_UNKNOWN_HOST: { - return ErrorType.ERROR_UNKNOWN_HOST; - } - case WebRequestError.ERROR_MALFORMED_URI: { - return ErrorType.ERROR_MALFORMED_URI; - } - case WebRequestError.ERROR_UNKNOWN_PROTOCOL: { - return ErrorType.ERROR_UNKNOWN_PROTOCOL; - } - case WebRequestError.ERROR_FILE_NOT_FOUND: { - return ErrorType.ERROR_FILE_NOT_FOUND; - } - case WebRequestError.ERROR_FILE_ACCESS_DENIED: { - return ErrorType.ERROR_FILE_ACCESS_DENIED; - } - } - } - case WebRequestError.ERROR_CATEGORY_PROXY: { - switch (error) { - case WebRequestError.ERROR_PROXY_CONNECTION_REFUSED: { - return ErrorType.ERROR_CONNECTION_REFUSED; - } - case WebRequestError.ERROR_UNKNOWN_PROXY_HOST: { - return ErrorType.ERROR_UNKNOWN_PROXY_HOST; - } - } - } - case WebRequestError.ERROR_CATEGORY_SAFEBROWSING: { - switch (error) { - case WebRequestError.ERROR_SAFEBROWSING_MALWARE_URI: { - return ErrorType.ERROR_SAFEBROWSING_MALWARE_URI; - } - case WebRequestError.ERROR_SAFEBROWSING_UNWANTED_URI: { - return ErrorType.ERROR_SAFEBROWSING_UNWANTED_URI; - } - case WebRequestError.ERROR_SAFEBROWSING_HARMFUL_URI: { - return ErrorType.ERROR_SAFEBROWSING_HARMFUL_URI; - } - case WebRequestError.ERROR_SAFEBROWSING_PHISHING_URI: { - return ErrorType.ERROR_SAFEBROWSING_PHISHING_URI; - } - } + case WebRequestError.ERROR_UNKNOWN_HOST: { + return ErrorType.ERROR_UNKNOWN_HOST; } + case WebRequestError.ERROR_MALFORMED_URI: { + return ErrorType.ERROR_MALFORMED_URI; + } + case WebRequestError.ERROR_UNKNOWN_PROTOCOL: { + return ErrorType.ERROR_UNKNOWN_PROTOCOL; + } + case WebRequestError.ERROR_FILE_NOT_FOUND: { + return ErrorType.ERROR_FILE_NOT_FOUND; + } + case WebRequestError.ERROR_FILE_ACCESS_DENIED: { + return ErrorType.ERROR_FILE_ACCESS_DENIED; + } + case WebRequestError.ERROR_PROXY_CONNECTION_REFUSED: { + return ErrorType.ERROR_PROXY_CONNECTION_REFUSED; + } + case WebRequestError.ERROR_UNKNOWN_PROXY_HOST: { + return ErrorType.ERROR_UNKNOWN_PROXY_HOST; + } + case WebRequestError.ERROR_SAFEBROWSING_MALWARE_URI: { + return ErrorType.ERROR_SAFEBROWSING_MALWARE_URI; + } + case WebRequestError.ERROR_SAFEBROWSING_UNWANTED_URI: { + return ErrorType.ERROR_SAFEBROWSING_UNWANTED_URI; + } + case WebRequestError.ERROR_SAFEBROWSING_HARMFUL_URI: { + return ErrorType.ERROR_SAFEBROWSING_HARMFUL_URI; + } + case WebRequestError.ERROR_SAFEBROWSING_PHISHING_URI: { + return ErrorType.ERROR_SAFEBROWSING_PHISHING_URI; + } + case WebRequestError.ERROR_CATEGORY_UNKNOWN: default: { return ErrorType.UNKNOWN; } @@ -142,16 +116,28 @@ public static PageResources create(int html, int css) { public static String createErrorPageDataURI(Context context, String uri, - int errorCategory, int errorType) { - // TODO: browser-error pages component needs to accept a uri parameter String html = ErrorPages.INSTANCE.createErrorPage( context, - fromGeckoErrorToErrorType(errorCategory, errorType), + fromGeckoErrorToErrorType(errorType), uri, R.raw.error_pages, R.raw.error_style); + boolean showSSLAdvanced; + switch (errorType) { + case WebRequestError.ERROR_SECURITY_SSL: + case WebRequestError.ERROR_SECURITY_BAD_CERT: + showSSLAdvanced = true; + break; + default: + showSSLAdvanced = false; + } + + html = html + .replace("%url%", uri) + .replace("%advancedSSLStyle%", showSSLAdvanced ? "block" : "none"); + return "data:text/html;base64," + Base64.encodeToString(html.getBytes(), Base64.NO_WRAP); } 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 970363ae4..7561aa15c 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/LocaleUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/LocaleUtils.java @@ -6,18 +6,19 @@ import android.os.Build; import androidx.annotation.NonNull; +import androidx.annotation.StringRes; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.browser.SettingsStore; -import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.ui.adapters.Language; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; import static android.os.Build.VERSION_CODES.N; @@ -27,10 +28,8 @@ public class LocaleUtils { private static Locale mSystemLocale; private static HashMap mLanguagesCache; - public static void init(Context context) { + public static void init() { getAllLanguages(); - SessionStore.get().setLocales( - LocaleUtils.getLanguageIdsFromList(getPreferredLanguages(context))); } public static void saveSystemLocale() { @@ -38,8 +37,8 @@ public static void saveSystemLocale() { } @NonNull - public static String getSystemLocale() { - return mSystemLocale.toLanguageTag(); + public static Locale getSystemLocale() { + return mSystemLocale; } @NonNull @@ -61,21 +60,37 @@ private static HashMap getAllLanguages() { mLanguagesCache.put(languageId, locale); } + Locale locale = Locale.forLanguageTag(getDeviceLocale().toLanguageTag()); + String languageId = locale.toLanguageTag(); + if (!mLanguagesCache.containsKey(languageId)) { + String displayName = locale.getDisplayName().substring(0, 1).toUpperCase() + locale.getDisplayName().substring(1); + Language language = new Language(languageId, displayName + " [" + languageId + "]"); + mLanguagesCache.put(languageId, language); + } + return mLanguagesCache; } public static void resetLanguages() { - String currentLocale = getCurrentLocale(); - mLanguagesCache.values().stream().forEach((language) -> { - language.setPreferred(language.getId().equals(currentLocale)); + mLanguagesCache.values().forEach((language) -> { + if (language == getDeviceLanguage()) { + language.setPreferred(true); + + } else { + language.setPreferred(false); + } }); } - public static Language getCurrentLocaleLanguage() { - return mLanguagesCache.get(getCurrentLocale()); + public static Language getDeviceLanguage() { + return mLanguagesCache.get(getDeviceLocale().toLanguageTag()); } - public static List getLanguageIdsFromList(@NonNull final List languages) { + public static Locale getDeviceLocale() { + return Resources.getSystem().getConfiguration().getLocales().get(0); + } + + public static List getLocalesFromLanguages(@NonNull final List languages) { List result = new ArrayList<>(); for (Language language : languages) { result.add(language.getId()); @@ -84,21 +99,31 @@ public static List getLanguageIdsFromList(@NonNull final List return result; } + public static List getPreferredLocales(@NonNull Context context) { + return LocaleUtils.getLocalesFromLanguages(LocaleUtils.getPreferredLanguages(context)); + } + public static List getPreferredLanguages(@NonNull Context aContext) { HashMap languages = getAllLanguages(); List savedLanguages = SettingsStore.getInstance(aContext).getContentLocales(); List preferredLanguages = new ArrayList<>(); if (savedLanguages != null) { for (String language : savedLanguages) { - Language lang = languages.get(language); + Language lang = languages.get(getClosestAvailableLocale(language)); + if (lang != null) { + lang.setPreferred(true); + preferredLanguages.add(lang); + } + } + + } + + if (savedLanguages == null || savedLanguages.isEmpty()) { + Language lang = languages.get(getClosestAvailableLocale(getDeviceLocale().toLanguageTag())); + if (lang != null) { lang.setPreferred(true); preferredLanguages.add(lang); } - - } else { - Language currentLanguage = getCurrentLocaleLanguage(); - currentLanguage.setPreferred(true); - preferredLanguages.add(currentLanguage); } return preferredLanguages; @@ -106,40 +131,52 @@ public static List getPreferredLanguages(@NonNull Context aContext) { public static List getAvailableLanguages() { HashMap languages = getAllLanguages(); - List availableLanguages = languages.values().stream() + return languages.values().stream() .sorted((o1, o2) -> o1.getName().compareToIgnoreCase(o2.getName())) .collect(Collectors.toList()); - - return availableLanguages; } @NonNull - public static List getSupportedVoiceLanguages(@NonNull Context aContext) { - return Arrays.asList(aContext.getResources().getStringArray( - R.array.developer_options_voice_search_languages_values)); - } - - @NonNull - public static List getSupportedDisplayLanguages(@NonNull Context aContext) { - return Arrays.asList(aContext.getResources().getStringArray( - R.array.developer_options_display_languages_values)); - } + public static String getClosestAvailableLocale(@NonNull String languageTag) { + List locales = Stream.of(Locale.getAvailableLocales()).collect(Collectors.toList()); + Locale inputLocale = Locale.forLanguageTag(languageTag); + Optional outputLocale = locales.stream().filter(item -> + item.equals(inputLocale) + ).findFirst(); + + if (!outputLocale.isPresent()) { + outputLocale = locales.stream().filter(item -> + item.getLanguage().equals(inputLocale.getLanguage()) && + item.getScript().equals(inputLocale.getScript()) && + item.getCountry().equals(inputLocale.getCountry()) + ).findFirst(); + } + if (!outputLocale.isPresent()) { + outputLocale = locales.stream().filter(item -> + item.getLanguage().equals(inputLocale.getLanguage()) && + item.getCountry().equals(inputLocale.getCountry()) + ).findFirst(); + } + if (!outputLocale.isPresent()) { + outputLocale = locales.stream().filter(item -> + item.getLanguage().equals(inputLocale.getLanguage()) + ).findFirst(); + } - @NonNull - public static String getDefaultVoiceSearchLocale(@NonNull Context aContext) { - String locale = getCurrentLocale(); - List supportedLanguages = getSupportedVoiceLanguages(aContext); - if (!supportedLanguages.contains(locale)) { - return supportedLanguages.get(0); + if (outputLocale.isPresent()) { + return outputLocale.get().toLanguageTag(); } - return locale; + return getDeviceLocale().toLanguageTag(); } @NonNull public static String getVoiceSearchLocale(@NonNull Context aContext) { String locale = SettingsStore.getInstance(aContext).getVoiceSearchLocale(); - return mapOldLocaleToNew(locale); + if (locale == null) { + locale = LocaleUtils.getDefaultSupportedLocale(aContext); + } + return locale; } public static void setVoiceSearchLocale(@NonNull Context context, @NonNull String locale) { @@ -147,25 +184,16 @@ public static void setVoiceSearchLocale(@NonNull Context context, @NonNull Strin } @NonNull - public static String getVoiceSearchLanguageString(@NonNull Context aContext) { - String language = LocaleUtils.getVoiceSearchLocale(aContext); - return getAllLanguages().get(language).getName(); - } - - @NonNull - public static String getDefaultDisplayLocale(@NonNull Context aContext) { - String locale = getCurrentLocale(); - List supportedLanguages = getSupportedDisplayLanguages(aContext); - if (!supportedLanguages.contains(locale)) { - return supportedLanguages.get(0); - } - - return locale; + public static Language getVoiceSearchLanguage(@NonNull Context aContext) { + return mLanguagesCache.get(getClosestAvailableLocale(getVoiceSearchLocale(aContext))); } @NonNull public static String getDisplayLocale(Context context) { String locale = SettingsStore.getInstance(context).getDisplayLocale(); + if (locale == null) { + locale = LocaleUtils.getDefaultSupportedLocale(context); + } return mapOldLocaleToNew((locale)); } @@ -174,12 +202,16 @@ public static void setDisplayLocale(@NonNull Context context, @NonNull String lo } @NonNull - public static String getDisplayCurrentLanguageString() { - return getAllLanguages().get(getCurrentLocale()).getName(); + public static Language getDisplayLanguage(@NonNull Context aContext) { + return mLanguagesCache.get(getClosestAvailableLocale(getDisplayLocale(aContext))); } public static Context setLocale(@NonNull Context context) { - return updateResources(context, SettingsStore.getInstance(context).getDisplayLocale()); + String locale = SettingsStore.getInstance(context).getDisplayLocale(); + if (locale == null) { + locale = LocaleUtils.getDefaultSupportedLocale(context); + } + return updateResources(context, locale); } private static Context updateResources(@NonNull Context context, @NonNull String language) { @@ -223,4 +255,105 @@ public static String mapToMozillaSpeechLocales(@NonNull String locale) { return locale; } + public static class LocalizedLanguage { + public @StringRes int name; + public Locale locale; + + private LocalizedLanguage() {} + + public static LocalizedLanguage create(@StringRes int name, @NonNull Locale locale) { + LocalizedLanguage language = new LocalizedLanguage(); + language.name = name; + language.locale = locale; + + return language; + } + } + + private static List localizedSupportedLanguages = Stream.of( + LocalizedLanguage.create(R.string.settings_language_english, new Locale("en", "US")), + LocalizedLanguage.create(R.string.settings_language_traditional_chinese, new Locale.Builder().setLanguage("zh").setScript("Hant").setRegion("TW").build()), + LocalizedLanguage.create(R.string.settings_language_simplified_chinese, new Locale.Builder().setLanguage("zh").setScript("Hans").setRegion("CN").build()), + LocalizedLanguage.create(R.string.settings_language_japanese, new Locale("ja", "JP")), + LocalizedLanguage.create(R.string.settings_language_french, new Locale("fr", "FR")), + LocalizedLanguage.create(R.string.settings_language_german, new Locale("de", "DE")), + LocalizedLanguage.create(R.string.settings_language_spanish, new Locale("es", "ES")), + LocalizedLanguage.create(R.string.settings_language_russian, new Locale("ru", "RU")), + LocalizedLanguage.create(R.string.settings_language_korean, new Locale("ko", "KR")), + LocalizedLanguage.create(R.string.settings_language_italian, new Locale("it", "IT")), + LocalizedLanguage.create(R.string.settings_language_danish, new Locale("da", "DK")), + LocalizedLanguage.create(R.string.settings_language_polish, new Locale("pl", "PL")), + LocalizedLanguage.create(R.string.settings_language_norwegian, new Locale("nb", "NO")), + LocalizedLanguage.create(R.string.settings_language_swedish, new Locale("sv", "SE")), + LocalizedLanguage.create(R.string.settings_language_finnish, new Locale("fi", "FI")), + LocalizedLanguage.create(R.string.settings_language_dutch, new Locale("nl", "NL")) + ).collect(Collectors.toList()); + + public static String[] getSupportedLocalizedLanguages(@NonNull Context context) { + return LocaleUtils.localizedSupportedLanguages.stream().map( + item -> StringUtils.capitalize(StringUtils.getStringByLocale(context, item.name, item.locale))). + collect(Collectors.toList()).toArray(new String[]{}); + } + + public static List getSupportedLocales() { + return LocaleUtils.localizedSupportedLanguages.stream().map( + item -> item.locale.toLanguageTag()). + collect(Collectors.toList()); + } + + public static int getIndexForSupportedLocale(@NonNull String locale) { + Optional locLang = localizedSupportedLanguages.stream().filter(item -> item.locale.toLanguageTag().equals(locale)).findFirst(); + return locLang.map(localizedLanguage -> localizedSupportedLanguages.indexOf(localizedLanguage)).orElse(0); + } + + public static String getSupportedLocalizedLanguageForIndex(@NonNull Context context, int index) { + return StringUtils.capitalize( + StringUtils.getStringByLocale( + context, + localizedSupportedLanguages.get(index).name, + localizedSupportedLanguages.get(index).locale)); + } + + public static String getSupportedLocaleForIndex(int index) { + return localizedSupportedLanguages.get(index).locale.toLanguageTag(); + } + + public static String getDefaultSupportedLocale(@NonNull Context context) { + return getClosestSupportedLocale(context, getDeviceLocale().toLanguageTag()); + } + + public static String getClosestSupportedLocale(@NonNull Context context, @NonNull String languageTag) { + Locale locale = Locale.forLanguageTag(languageTag); + Optional language = LocaleUtils.localizedSupportedLanguages.stream().filter(item -> + item.locale.equals(locale) + ).findFirst(); + + if (!language.isPresent()) { + language = LocaleUtils.localizedSupportedLanguages.stream().filter(item -> + item.locale.getLanguage().equals(locale.getLanguage()) && + item.locale.getScript().equals(locale.getScript()) && + item.locale.getCountry().equals(locale.getCountry()) + ).findFirst(); + } + if (!language.isPresent()) { + language = LocaleUtils.localizedSupportedLanguages.stream().filter(item -> + item.locale.getLanguage().equals(locale.getLanguage()) && + item.locale.getCountry().equals(locale.getCountry()) + ).findFirst(); + } + if (!language.isPresent()) { + language = LocaleUtils.localizedSupportedLanguages.stream().filter(item -> + item.locale.getLanguage().equals(locale.getLanguage()) + ).findFirst(); + } + + if (language.isPresent()) { + return language.get().locale.toLanguageTag(); + + } else { + // If there is no closest supported locale we fallback to en-US + return "en-US"; + } + } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/utils/SystemUtils.java b/app/src/common/shared/org/mozilla/vrbrowser/utils/SystemUtils.java index e5503348c..5ab608c6f 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/SystemUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/SystemUtils.java @@ -4,14 +4,30 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.util.Log; import androidx.annotation.NonNull; +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.geckoview.CrashReporter; +import org.mozilla.geckoview.GeckoResult; import org.mozilla.vrbrowser.BuildConfig; +import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.VRBrowserActivity; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + public class SystemUtils { + private static final String LOGTAG = SystemUtils.createLogtag(SystemUtils.class); + public static final long ONE_DAY_MILLIS = 86400000; public static final long TWO_DAYS_MILLIS = 172800000; public static final long ONE_WEEK_MILLIS = 604800000; @@ -40,4 +56,60 @@ public static final void scheduleRestart(@NonNull Context context, long delay) { public static String createLogtag(@NonNull Class aClass) { return "VRB[" + aClass.getSimpleName() + "]"; } + + private static final String CRASH_STATS_URL = "https://crash-stats.mozilla.com/report/index/"; + + private static void sendCrashFiles(@NonNull Context context, @NonNull final String aDumpFile, @NonNull final String aExtraFile) { + ThreadUtils.postToBackgroundThread(() -> { + try { + GeckoResult result = CrashReporter.sendCrashReport(context, new File(aDumpFile), new File(aExtraFile), context.getString(R.string.crash_app_name)); + + result.accept(crashID -> { + Log.e(LOGTAG, "Submitted crash report id: " + crashID); + Log.e(LOGTAG, "Report available at: " + CRASH_STATS_URL + crashID); + }, ex -> { + Log.e(LOGTAG, "Failed to submit crash report: " + (ex != null ? ex.getMessage() : "Exception is NULL")); + }); + } catch (IOException | URISyntaxException e) { + Log.e(LOGTAG, "Failed to send crash report: " + e.toString()); + } + }); + } + + public static void postCrashFiles(@NonNull Context context, @NonNull final String aDumpFile, @NonNull final String aExtraFile) { + sendCrashFiles(context, aDumpFile, aExtraFile); + } + + public static void postCrashFiles(@NonNull Context context, final ArrayList aFiles) { + for (String file: aFiles) { + try { + ArrayList list = new ArrayList<>(2); + try (FileInputStream in = context.openFileInput(file)) { + try(BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + String line; + while((line = br.readLine()) != null) { + list.add(line); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + if (list.size() < 2) { + Log.e(LOGTAG, "Failed read crash dump file names from: " + file); + return; + } + sendCrashFiles(context, list.get(0), list.get(1)); + } finally { + Log.d(LOGTAG,"Removing crash file: " + file); + context.deleteFile(file); + } + } + } + + public static void clearCrashFiles(@NonNull Context context, final ArrayList aFiles) { + for (String file : aFiles) { + Log.e(LOGTAG, "Deleting crash file: " + file); + context.deleteFile(file); + } + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/utils/UrlUtils.java b/app/src/common/shared/org/mozilla/vrbrowser/utils/UrlUtils.java index 1299d5777..a75364572 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/UrlUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/UrlUtils.java @@ -5,10 +5,10 @@ package org.mozilla.vrbrowser.utils; -import java.util.regex.Pattern; - import androidx.annotation.Nullable; +import java.util.regex.Pattern; + // This class refers from mozilla-mobile/focus-android public class UrlUtils { 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 d4cb84f02..a6e367e99 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/ViewUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/ViewUtils.java @@ -1,6 +1,12 @@ package org.mozilla.vrbrowser.utils; -import android.graphics.Color; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; import android.os.Build; import android.text.Html; import android.text.Layout; @@ -9,7 +15,6 @@ import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.URLSpan; -import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -18,10 +23,8 @@ import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.coordinatorlayout.widget.ViewGroupUtils; import androidx.core.text.HtmlCompat; -import org.jetbrains.annotations.NotNull; import org.mozilla.vrbrowser.ui.widgets.UIWidget; public class ViewUtils { @@ -116,7 +119,7 @@ public static boolean isEqualOrChildrenOf(@NonNull ViewGroup aParent, @NonNull V return isChildrenOf(aParent, aView); } - public static boolean isInsideView(@NotNull View view, int rx, int ry) { + public static boolean isInsideView(@NonNull View view, int rx, int ry) { int[] location = new int[2]; view.getLocationOnScreen(location); int x = location[0]; @@ -213,4 +216,26 @@ public boolean onTouch(View view, MotionEvent event) { public static void setStickyClickListener(@NonNull View aView, View.OnClickListener aCallback) { aView.setOnTouchListener(new StickyClickListener(aCallback)); } + + @NonNull + public static Bitmap getRoundedCroppedBitmap(@NonNull Bitmap bitmap) { + int widthLight = bitmap.getWidth(); + int heightLight = bitmap.getHeight(); + + Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(output); + Paint paintColor = new Paint(); + paintColor.setFlags(Paint.ANTI_ALIAS_FLAG); + + RectF rectF = new RectF(new Rect(0, 0, widthLight, heightLight)); + + canvas.drawRoundRect(rectF, widthLight / 2 ,heightLight / 2,paintColor); + + Paint paintImage = new Paint(); + paintImage.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP)); + canvas.drawBitmap(bitmap, 0, 0, paintImage); + + return output; + } } diff --git a/app/src/googlevr/cpp/DeviceDelegateGoogleVR.cpp b/app/src/googlevr/cpp/DeviceDelegateGoogleVR.cpp index 047dbaa12..b7d83ade3 100644 --- a/app/src/googlevr/cpp/DeviceDelegateGoogleVR.cpp +++ b/app/src/googlevr/cpp/DeviceDelegateGoogleVR.cpp @@ -394,7 +394,8 @@ DeviceDelegateGoogleVR::RegisterImmersiveDisplay(ImmersiveDisplayPtr aDisplay) { } m.immersiveDisplay->SetDeviceName("Daydream"); - m.immersiveDisplay->SetCapabilityFlags(device::Position | device::Orientation | device::Present | device::StageParameters); + m.immersiveDisplay->SetCapabilityFlags(device::Position | device::Orientation | device::Present | device::StageParameters | + device::InlineSession | device::ImmersiveVRSession); m.immersiveDisplay->SetSittingToStandingTransform(vrb::Matrix::Translation(kAverageHeight)); gvr_sizei size = m.GetRecommendedImmersiveModeSize(); m.immersiveDisplay->SetEyeResolution(size.width / 2, size.height); diff --git a/app/src/main/assets/userAgentOverride.json b/app/src/main/assets/userAgentOverride.json index 044ac11ea..79ede3a9e 100644 --- a/app/src/main/assets/userAgentOverride.json +++ b/app/src/main/assets/userAgentOverride.json @@ -1,5 +1,6 @@ { "744723238cc3c00582d7ff9e8ece83e6503a3926d8c07491672d5da3634be41e1d41580dd3694b663c0d0a5e64280a2b411a1346e6ed0d53d694835781cf6436": "Mozilla/5.0 (X11; Linux x86_64; rv:69) Gecko/20100101 Firefox/69.0", - "ae0755740e4354ac67025056e775ad06d8a529ae4f37244fbb02d72199e2c780311e47aa9895079b980ec4bfa676f1f39c4ab41ea995c524e52bde9a73623da2": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12) AppleWebKit/602.1.21 (KHTML, like Gecko) Version/9.2 Safari/602.1.21", - "e6137b4c2f49a3917c2c90a50fb270a5eebb962f2c72344ae2e29e321bb21891e5ca4fec06cae78e14f4a8510473e934234e9ec3f60e8415f5f6da754c55b9b1": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12) AppleWebKit/602.1.21 (KHTML, like Gecko) Version/9.2 Safari/602.1.21" + "ae0755740e4354ac67025056e775ad06d8a529ae4f37244fbb02d72199e2c780311e47aa9895079b980ec4bfa676f1f39c4ab41ea995c524e52bde9a73623da2": "Mozilla/5.0 (Linux; Android 7.1.1; Quest) AppleWebKit/537.36 (KHTML, like Gecko) OculusBrowser/7.0.13.186866463 SamsungBrowser/4.0 Chrome/77.0.3865.126 Mobile VR Safari/537.36", + "e6137b4c2f49a3917c2c90a50fb270a5eebb962f2c72344ae2e29e321bb21891e5ca4fec06cae78e14f4a8510473e934234e9ec3f60e8415f5f6da754c55b9b1": "Mozilla/5.0 (Linux; Android 7.1.1; Quest) AppleWebKit/537.36 (KHTML, like Gecko) OculusBrowser/7.0.13.186866463 SamsungBrowser/4.0 Chrome/77.0.3865.126 Mobile VR Safari/537.36" } + \ No newline at end of file diff --git a/app/src/main/assets/web_extensions/webcompat_vimeo/main.js b/app/src/main/assets/web_extensions/webcompat_vimeo/main.js index 412e50046..7e3f925cf 100644 --- a/app/src/main/assets/web_extensions/webcompat_vimeo/main.js +++ b/app/src/main/assets/web_extensions/webcompat_vimeo/main.js @@ -331,6 +331,10 @@ try { if (newObjData.name === 'window.vimeo.clip_page_config') { if (configObj.clip.canvas === 1 || configObj.clip.is_spatial === true) { is360 = true; + // Detect 360 stereo videos + if (configObj.clip.title.toLowerCase().indexOf('stereo') >= 0) { + QS_DEFAULTS.mozVideoProjection = "360s_auto"; + } } configObj.clip.canvas = 1; diff --git a/app/src/main/assets/web_extensions/webcompat_youtube/background.js b/app/src/main/assets/web_extensions/webcompat_youtube/background.js new file mode 100644 index 000000000..233331a6e --- /dev/null +++ b/app/src/main/assets/web_extensions/webcompat_youtube/background.js @@ -0,0 +1,25 @@ +const CUSTOM_USER_AGENT = 'Mozilla/5.0 (Linux; Android 7.1.1; Quest) AppleWebKit/537.36 (KHTML, like Gecko) OculusBrowser/7.0.13.186866463 SamsungBrowser/4.0 Chrome/77.0.3865.126 Mobile VR Safari/537.36'; +const targetUrls = [ + "https://*.youtube.com/*", + "https://*.youtube-nocookie.com/*" +]; + +/** + * Override UA. This is required to get the equirectangular video formats from Youtube. + * Otherwise youtube uses custom 360 controls. + */ +function overrideUA(req) { + for (const header of req.requestHeaders) { + if (header.name.toLowerCase() === "user-agent") { + header.value = CUSTOM_USER_AGENT; + } + } + return { requestHeaders: req.requestHeaders }; +} + +browser.webRequest.onBeforeSendHeaders.addListener( + overrideUA, + { urls: targetUrls }, + ["blocking", "requestHeaders"] + ); + \ No newline at end of file diff --git a/app/src/main/assets/web_extensions/webcompat_youtube/main.css b/app/src/main/assets/web_extensions/webcompat_youtube/main.css index 03b410669..7ec4803a8 100644 --- a/app/src/main/assets/web_extensions/webcompat_youtube/main.css +++ b/app/src/main/assets/web_extensions/webcompat_youtube/main.css @@ -4,3 +4,52 @@ .ytp-generic-popup, ytp-generic-popup { display: none; } +/* Hide not working share button in fullscreen. */ +.ytp-share-icon { + display: none; +} + + +/* Fix Youtube homepage is wrongly displayed on only one column + See https://github.com/MozillaReality/FirefoxReality/issues/2595 */ +ytd-rich-grid-video-renderer { + min-width: 0px; +} + +@media screen and (max-width: 700px) { + ytd-rich-grid-renderer { + --ytd-rich-grid-items-per-row: 1 !important; + --ytd-rich-grid-posts-per-row: 1 !important; + } +} + +@media screen and (max-width: 899px) and (min-width: 701px) { + ytd-rich-grid-renderer { + --ytd-rich-grid-items-per-row: 2 !important; + --ytd-rich-grid-posts-per-row: 2 !important; + } +} + +@media screen and (max-width: 1115px) and (min-width: 900px) { + ytd-rich-grid-renderer { + --ytd-rich-grid-items-per-row: 3 !important; + --ytd-rich-grid-posts-per-row: 3 !important; + } +} + +@media screen and (min-width: 1116px) { + ytd-rich-grid-renderer { + --ytd-rich-grid-items-per-row: 4 !important; + --ytd-rich-grid-posts-per-row: 4 !important; + } +} + + +/* Youtube Videos tagged with Stereo VR have a layout problem, they don't take the whole area + of the video. This styles are used to make sure the video has the correct size for immersive + playback */ +div.ytp-fullscreen video, .fxr-vr-video { + width: 100% !important; + left: 0px !important; +} + diff --git a/app/src/main/assets/web_extensions/webcompat_youtube/main.js b/app/src/main/assets/web_extensions/webcompat_youtube/main.js index 8eb2357bc..d439000fa 100644 --- a/app/src/main/assets/web_extensions/webcompat_youtube/main.js +++ b/app/src/main/assets/web_extensions/webcompat_youtube/main.js @@ -1,312 +1,236 @@ -const CUSTOM_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12) AppleWebKit/602.1.21 (KHTML, like Gecko) Version/9.2 Safari/602.1.21'; +'use strict'; +const CUSTOM_USER_AGENT = 'Mozilla/5.0 (Linux; Android 7.1.1; Quest) AppleWebKit/537.36 (KHTML, like Gecko) OculusBrowser/7.0.13.186866463 SamsungBrowser/4.0 Chrome/77.0.3865.126 Mobile VR Safari/537.36'; const LOGTAG = '[firefoxreality:webcompat:youtube]'; +const VIDEO_PROJECTION_PARAM = 'mozVideoProjection'; const YT_SELECTORS = { disclaimer: '.yt-alert-message, yt-alert-message', - moviePlayer: '#movie_player' + player: '#movie_player', + embedPlayer: '.html5-video-player', + largePlayButton: '.ytp-large-play-button', + thumbnail: '.ytp-cued-thumbnail-overlay-image', + embedTitle: '.ytp-title-text' }; -const YT_PATHS = { - watch: '/watch' -}; - -try { - // Note: À la Oculus Browser, we intentionally use this particular `User-Agent` string - // for YouTube to force the most optimal, high-resolution layout available for playback in a mobile VR browser. - Object.defineProperty(navigator, 'userAgent', { - get: () => CUSTOM_USER_AGENT - }); - - // If missing, inject a `` tag to trigger YouTube's mobile layout. - let viewportEl = document.querySelector('meta[name="viewport"]'); - if (!viewportEl) { - document.documentElement.insertAdjacentHTML('afterbegin', - ``); - } - - let is360 = null; - let qs = new URLSearchParams(window.location.search); - let retryTimeout = null; - - const prefs = { - hd: false, - quality: 1440, - log: qs.get('mozDebug') !== '0' && qs.get('mozdebug') !== '0' && qs.get('debug') !== '0', - retryAttempts: parseInt(qs.get('retryAttempts') || qs.get('retryattempts') || '10', 10), - retryTimeout: parseInt(qs.get('retryTimeout') || qs.get('retrytimeout') || '500', 10) - }; - - const printLog = String(prefs.log) === 'true'; - - const log = (...args) => printLog && console.log(LOGTAG, ...args); - const logError = (...args) => printLog && console.error(LOGTAG, ...args); - const logWarn = (...args) => printLog && console.warn(LOGTAG, ...args); - - let auto360 = true; - - const onNavigate = (delayTime = 500) => setTimeout(() => { - ytImprover360(auto360); - - ytImprover.completed = false; - ytImprover(1); - }, delayTime); - - window.addEventListener('fullscreenchange', () => { - auto360 = !!document.fullscreenElement; - ytImprover360(auto360); - }); - - window.addEventListener('load', () => { - viewportEl = document.querySelector('meta[name="viewport"]:not([data-fxr-injected])'); - if (viewportEl) { - viewportEl.parentNode.removeChild(viewportEl); +const ENABLE_LOGS = true; +const logDebug = (...args) => ENABLE_LOGS && console.log(LOGTAG, ...args); +const logError = (...args) => ENABLE_LOGS && console.error(LOGTAG, ...args); + +class YoutubeExtension { + // We set a custom UA to force Youtube to display the most optimal + // and high-resolution layout available for playback in a mobile VR browser. + overrideUA() { + Object.defineProperty(navigator, 'userAgent', { + get: () => CUSTOM_USER_AGENT + }); + logDebug(`Youtube UA overriden to: ${navigator.userAgent}`) + } + + // If missing, inject a `` tag to trigger YouTube's mobile layout. + overrideViewport() { + const content = `width=device-width;maximum-scale=1;minimum-scale=1;initial-scale=1;`; + let viewport = document.querySelector('meta[name="viewport"]'); + if (viewport) { + viewport.setAttribute('content', content); + } else { + const container = document.head || document.documentElement; + container.insertAdjacentHTML('afterbegin', ``); + } + logDebug(`Youtube viewport updated: ${window.innerWidth}x${window.innerHeight} `); } - // Wait until video has loaded the first frame to force quality change. - // This prevents the infinite spinner problem. - // See https://github.com/MozillaReality/FirefoxReality/issues/1433 - var video = document.getElementsByTagName("video")[0]; - if (video.readyState >= 2) { - onNavigate(0); - } else { - video.addEventListener("loadeddata", () => onNavigate(0)); + // Select a better youtube video quality + overrideQuality() { + logDebug('overrideQuality attempt'); + const player = this.getPlayer(); + if (!player) { + logDebug('player not ready'); + return false; + } + const preferredLevels = this.getPreferredQualities(); + const currentLevel = player.getPlaybackQuality(); + logDebug(`Video getPlaybackQuality: ${currentLevel}`); + + let availableLevels = player.getAvailableQualityLevels(); + logDebug(`Video getAvailableQualityLevels: ${availableLevels}`); + for (const level of preferredLevels) { + if (availableLevels.indexOf(level) >= 0) { + if (currentLevel !== level) { + player.setPlaybackQualityRange(level, level); + logDebug(`Video setPlaybackQualityRange: ${level}`); + } else { + logDebug('Best quality already selected'); + } + return true; + } + } + return false; } - }); - window.addEventListener('pushstate', onNavigate); - - window.addEventListener('popstate', onNavigate); - - window.addEventListener('click', evt => { - if (!window.location.pathname.startsWith(YT_PATHS.watch)) { - return; - } - if (is360 && evt.target.closest(YT_SELECTORS.moviePlayer) && !evt.target.closest('.ytp-chrome-bottom')) { - const playerEl = document.querySelector(YT_SELECTORS.moviePlayer); - if (!playerEl) { - return; - } - playerEl.requestFullscreen(); + overrideQualityRetry() { + this.retry("overrideQuality", () => this.overrideQuality()); } - }); - function ytImprover360 (auto) { - if (!window.location.pathname.startsWith(YT_PATHS.watch)) { - is360 = false; - return; + // Automatically select a video projection if needed + overrideVideoProjection() { + if (!this.isWatchingPage()) { + logDebug("is not watching page"); + return; // Only override projection in the Youtube watching page. + } + const qs = new URLSearchParams(window.location.search); + if (qs.get(VIDEO_PROJECTION_PARAM)) { + logDebug(`Video has already a video projection selected: ${qs.get(VIDEO_PROJECTION_PARAM)}`); + this.updateVideoStyle(); + return; + } + // There is no standard API to detect video projection yet. + // Try to infer it from the video disclaimer or title for now. + const targets = [ + document.querySelector(YT_SELECTORS.disclaimer), + document.querySelector(YT_SELECTORS.embedTitle) + ]; + const is360 = targets.some((node) => node && node.textContent.includes('360')); + if (is360) { + const stereo = targets.some((node) => node && node.textContent.toLowerCase().includes('stereo')); + qs.set('mozVideoProjection', stereo ? '360s_auto' : '360_auto'); + this.updateURL(qs); + this.updateVideoStyle(); + logDebug(`Video projection set to: ${qs.get(VIDEO_PROJECTION_PARAM)}`); + } else { + logDebug(`Video is flat, no projection selected`); + } } - const disclaimerEl = document.querySelector(YT_SELECTORS.disclaimer); - is360 = disclaimerEl ? disclaimerEl.textContent.includes('360') : false; - - if (!is360) { - return; + updateVideoStyle() { + const video = this.getVideo(); + if (video) { + video.classList.add('fxr-vr-video'); + logDebug('Added video projection style'); + } } - qs = new URLSearchParams(window.location.search); - - const currentProjection = (qs.get('mozVideoProjection') || '').toLowerCase(); - qs.delete('mozVideoProjection'); - switch (currentProjection) { - case '360': - case '360_auto': - case '360s': - case '360s_auto': - case '180': - case '180_auto': - case '180lr': - case '180lr_auto': - case '180tb': - case '180tb_auto': - qs.set('mozVideoProjection', currentProjection); - break; - default: - qs.set('mozVideoProjection', auto ? '360_auto' : '360'); - break; + overrideClick(event) { + this.overrideVideoProjection(); + const player = this.getPlayer(); + if (!this.isWatchingPage() || !this.hasVideoProjection() || document.fullscreenElement || !player) { + return; // Only override click in the Youtube watching page for 360 videos. + } + if (this.isEmbed() && !this.isVideoReady()) { + return false; // Embeds videos are only loaded after the first click + } + const target = event.target; + let valid = target.tagName.toLowerCase() === 'video' || + target === document.querySelector(YT_SELECTORS.thumbnail) || + target === document.querySelector(YT_SELECTORS.largePlayButton) || + target == player; + + if (valid) { + player.playVideo(); + player.requestFullscreen(); + // Force video play when entering immersive mode. + setTimeout(() => this.retry("PlayVideo", () => { + player.playVideo(); + return !this.getVideo().paused; + }), 200); + } } - const newUrl = getNewUrl(qs); - if (newUrl && (window.location.pathname + window.location.search) !== newUrl) { - window.history.replaceState({}, document.title, newUrl); - return newUrl; + // Runs the callback when the video is ready (has loaded the first frame). + waitForVideoReady(callback) { + this.retry("VideoReady", () => { + const video = this.getVideo(); + if (!video) { + return false; + } + if (video.readyState >= 2) { + callback(); + } else { + video.addEventListener("loadeddata", callback, {once: true}); + } + return true; + }); + } + + // Get's the Youtube player elements which contains the API functions. + getPlayer() { + let player = document.querySelector(this.isEmbed() ? YT_SELECTORS.embedPlayer : YT_SELECTORS.player); + if (!player || !player.wrappedJSObject) { + return null; + } + return player.wrappedJSObject; } - } - function getNewUrl (qs) { - let newUrl = `${window.location.pathname}`; - if (qs) { - newUrl = `${newUrl}?${qs}`; + getVideo() { + return document.getElementsByTagName('video')[0]; } - return newUrl; - } - const ytImprover = window.ytImprover = (state, attempts) => { - if (!window.location.pathname.startsWith(YT_PATHS.watch)) { - ytImprover.completed = true; - return; - } - if (ytImprover.completed) { - return; + // Get's the preferred video qualities for the current device. + getPreferredQualities() { + let all = ['hd2880', 'hd2160','hd1440', 'hd1080', 'hd720', 'large', 'medium']; + return all; } - if (typeof attempts === 'undefined') { - attempts = 1; - } - if (attempts >= prefs.retryAttempts) { - logError(`Giving up trying to increase resolution after ${prefs.retryAttempts} attempts.`); - return; + // Returns true if we are in a video watching page. + isWatchingPage() { + return window.location.pathname.startsWith('/watch') || this.isEmbed(); } - let player = document.querySelector(YT_SELECTORS.moviePlayer); - let reason = 'unknown'; - if (state !== 1) { - reason = 'invalid state'; - } else if (!player) { - reason = 'player not found'; - } else if (!player.wrappedJSObject) { - reason = 'player.wrappedJSObject not found'; - player = null; - } else if (!player.wrappedJSObject.getAvailableQualityLevels) { - reason = 'player.wrappedJSObject.getAvailableQualityLevels not found'; - player = null; + isEmbed() { + return window.location.pathname.startsWith('/embed'); } - if (!player) { - logWarn(`Cannot find player because ${reason}. attempts: ${attempts}`); - attempts++; - retryTimeout = setTimeout(() => { - ytImprover(state, attempts); - }, prefs.retryTimeout); - return; + // Returns true if we are in a video watching page. + hasVideoProjection() { + const qs = new URLSearchParams(window.location.search); + return !!qs.get(VIDEO_PROJECTION_PARAM); } - player = player.wrappedJSObject; - - const levels = player.getAvailableQualityLevels(); - if (!levels || !levels.length) { - logWarn(`Cannot read 'player.getAvailableQualityLevels()' attempts: ${attempts}`); - attempts++; - retryTimeout = setTimeout(() => { - ytImprover(state, attempts); - }, prefs.retryTimeout); - return; + isVideoReady() { + const video = this.getVideo(); + return video && video.readyState >=2; } - clearTimeout(retryTimeout); - ytImprover.completed = true; - - prefs.qualities = [ - 'highres', 'h2880', 'hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'auto' - ]; - prefs.qualityLabels = { - '4320': 'highres', // 8K / 4320p / QUHD - '2880': 'hd2880', // 5K / 2880p / UHD+ - '2160': 'hd2160', // 4K / 2160p / UHD - '1440': 'hd1440', // 1440p / QHD - '1080': 'hd1080', // 1080p / FHD - '720': 'hd720', // 720p / HD - '480': 'large', // 480p - '360': 'medium', // 360p - '240': 'small', // 240p - '144': 'tiny', // 144p - '0': 'auto' - }; - - const getDesiredQuality = () => { - const qsQuality = (qs.get('vq') || qs.get('quality') || '').trim().toLowerCase(); - if (qsQuality) { - if (qsQuality in prefs.qualityLabels) { - prefs.quality = prefs.qualityLabels[qsQuality]; - } else { - const qsQualityNumber = parseInt(qsQuality, 10); - if (Number.isInteger(qsQualityNumber)) { - prefs.quality = qsQualityNumber; - } else { - prefs.quality = qsQuality; - } + // Utility function to retry tasks max n times until the execution is successful. + retry(taskName, task, attempts = 10, interval = 200) { + let succeeded = false; + try { + succeeded = task(); + } catch (ex) { + logError(`Got exception runnning ${taskName} task: ${ex}`); } - } - prefs.quality = String(prefs.quality).toLowerCase(); - if (qsQuality === 'auto' || qsQuality === 'default') { - prefs.quality = 'auto'; - } - if (prefs.quality in prefs.qualityLabels) { - prefs.quality = prefs.qualityLabels[prefs.quality]; - } - return prefs.quality; - }; - - prefs.quality = getDesiredQuality(); - if (prefs.quality === 'auto') { - return log(`Desired quality is fine (${prefs.quality})`); - } - - const currentQuality = player.getPlaybackQuality(); - if (prefs.quality === currentQuality) { - return log(`Current quality is desired quality (${currentQuality})`); - } - - const findBestQuality = increase => { - if (prefs.quality === 'highest' || prefs.quality === 'best' || prefs.quality === 'max' || prefs.quality === 'maximum') { - return levels[0]; - } - if (prefs.quality === 'lowest' || prefs.quality === 'worst' || prefs.quality === 'min' || prefs.quality === 'minimum') { - return levels[levels.length - 1]; - } - if (increase) { - prefs.quality = prefs.qualities[prefs.qualities.indexOf(prefs.quality) - 1] || levels[0]; - } - const index = levels.indexOf(prefs.quality); - if (index !== -1) { - return prefs.quality; - } - return findBestQuality(true); - }; - const newBestQuality = findBestQuality(); - if (currentQuality === newBestQuality) { - return log(`Current quality "${currentQuality}" is the best available quality`); - } - - if (!player.setPlaybackQuality) { - return logError('`player.setPlaybackQuality` not available'); - } - player.setPlaybackQuality(newBestQuality); - - if (!player.setPlaybackQualityRange) { - return logError('`player.setPlaybackQualityRange` not available'); - } - try { - player.setPlaybackQualityRange(newBestQuality, newBestQuality); - } catch (e) { - logError(`Failed to call 'player.setPlaybackQualityRange(${newBestQuality}, ${newBestQuality})' with exception: `, e); - return; + if (succeeded) { + logDebug(`${taskName} succeeded`); + return; + } + attempts--; + logDebug(`${taskName} failed. Remaining attempts ${attempts}`); + if (attempts > 0) { + setTimeout(() => { + this.retry(taskName, task, attempts, interval); + }) + }; + } + // Utility function to replace current URL params and update history. + updateURL(qs) { + const newUrl = `${window.location.pathname}?${qs}`; + window.history.replaceState({}, document.title, newUrl); + logDebug(`update URL to ${newUrl}`); } +} - log(`Changed quality from "${currentQuality}" to "${newBestQuality}"`); - }; - - window.wrappedJSObject.onYouTubePlayerReady = evt => { - log('`onYouTubePlayerReady` called'); - window.ytImprover(1); - evt.addEventListener('onStateChange', 'ytImprover'); - ytImprover360(true); - }; - - window.addEventListener('spfready', () => { - log('`spfready` event fired'); - if (window.wrappedJSObject.ytplayer && window.wrappedJSObject.ytplayer.config) { - log('`window.ytplayer.config.args.jsapicallback` set'); - window.wrappedJSObject.ytplayer.config.args.jsapicallback = 'onYouTubePlayerReady'; +logDebug(`Initializing youtube extension in frame: ${window.location.href}`); +const youtube = new YoutubeExtension(); +youtube.overrideUA(); +youtube.overrideViewport(); +window.addEventListener('load', () => { + logDebug('page load'); + youtube.overrideVideoProjection(); + // Wait until video has loaded the first frame to force quality change. + // This prevents the infinite spinner problem. + // See https://github.com/MozillaReality/FirefoxReality/issues/1433 + if (youtube.isWatchingPage()) { + youtube.waitForVideoReady(() => youtube.overrideQualityRetry()); } - }); - - window.addEventListener("beforeunload", function (e) { - // Make sure that the disable_polymer parameter is kept. Youtube processes the parameter but removes it from the URL. - // See https://github.com/MozillaReality/FirefoxReality/issues/1426 - let qs = new URLSearchParams(window.location.search); - qs.set('disable_polymer', '1'); - let url = getNewUrl(qs); - window.history.replaceState({}, document.title, url); - }); +}); -} catch (err) { - console.error(LOGTAG, 'Encountered error:', err); -} +window.addEventListener('pushstate', () => youtube.overrideVideoProjection()); +window.addEventListener('popstate', () => youtube.overrideVideoProjection()); +window.addEventListener('click', event => youtube.overrideClick(event)); diff --git a/app/src/main/assets/web_extensions/webcompat_youtube/manifest.json b/app/src/main/assets/web_extensions/webcompat_youtube/manifest.json index 9e3daea7d..34d24e240 100644 --- a/app/src/main/assets/web_extensions/webcompat_youtube/manifest.json +++ b/app/src/main/assets/web_extensions/webcompat_youtube/manifest.json @@ -18,5 +18,9 @@ "run_at": "document_start", "all_frames": true } - ] + ], + "permissions": ["webRequest", "webRequestBlocking", "*://*.youtube.com/*", "*://*.youtube-nocookie.com/*"], + "background": { + "scripts": ["background.js"] + } } diff --git a/app/src/main/cpp/BrowserWorld.cpp b/app/src/main/cpp/BrowserWorld.cpp index 1fa4b6578..a96108fd1 100644 --- a/app/src/main/cpp/BrowserWorld.cpp +++ b/app/src/main/cpp/BrowserWorld.cpp @@ -871,6 +871,7 @@ BrowserWorld::Draw() { m.device->ProcessEvents(); m.context->Update(); m.externalVR->PullBrowserState(); + m.externalVR->SetHapticState(m.controllers); m.CheckExitImmersive(); if (m.splashAnimation) { @@ -1042,6 +1043,16 @@ BrowserWorld::UpdateWidget(int32_t aHandle, const WidgetPlacementPtr& aPlacement LayoutWidget(aHandle); } +void +BrowserWorld::UpdateWidgetRecursive(int32_t aHandle, const WidgetPlacementPtr& aPlacement) { + UpdateWidget(aHandle, aPlacement); + for (WidgetPtr& widget: m.widgets) { + if (widget->GetPlacement() && widget->GetPlacement()->parentHandle == aHandle) { + UpdateWidgetRecursive(widget->GetHandle(), widget->GetPlacement()); + } + } +} + void BrowserWorld::RemoveWidget(int32_t aHandle) { ASSERT_ON_RENDER_THREAD(); @@ -1469,13 +1480,21 @@ BrowserWorld::CreateSkyBox(const std::string& aBasePath, const std::string& aExt ASSERT_ON_RENDER_THREAD(); vrb::PausePerformanceMonitor pauseMonitor(*m.monitor); const bool empty = aBasePath == "cubemap/void"; + if (empty) { + if (m.skybox) { + VRLayerCubePtr layer = m.skybox->GetLayer(); + if (layer) { + m.device->DeleteLayer(layer); + } + m.skybox->GetRoot()->RemoveFromParents(); + m.skybox = nullptr; + } + return; + } const std::string extension = aExtension.empty() ? ".ktx" : aExtension; const GLenum glFormat = extension == ".ktx" ? GL_COMPRESSED_RGB8_ETC2 : GL_RGBA8; const int32_t size = 1024; - if (m.skybox && empty) { - m.skybox->SetVisible(false); - return; - } else if (m.skybox) { + if (m.skybox) { m.skybox->SetVisible(true); if (m.skybox->GetLayer() && (m.skybox->GetLayer()->GetWidth() != size || m.skybox->GetLayer()->GetFormat() != glFormat)) { VRLayerCubePtr oldLayer = m.skybox->GetLayer(); @@ -1484,8 +1503,7 @@ BrowserWorld::CreateSkyBox(const std::string& aBasePath, const std::string& aExt m.device->DeleteLayer(oldLayer); } m.skybox->Load(m.loader, aBasePath, extension); - return; - } else if (!empty) { + } else { VRLayerCubePtr layer = m.device->CreateLayerCube(size, size, glFormat); m.skybox = Skybox::Create(m.create, layer); m.rootOpaqueParent->AddNode(m.skybox->GetRoot()); @@ -1514,7 +1532,7 @@ JNI_METHOD(void, updateWidgetNative) (JNIEnv* aEnv, jobject, jint aHandle, jobject aPlacement) { crow::WidgetPlacementPtr placement = crow::WidgetPlacement::FromJava(aEnv, aPlacement); if (placement) { - crow::BrowserWorld::Instance().UpdateWidget(aHandle, placement); + crow::BrowserWorld::Instance().UpdateWidgetRecursive(aHandle, placement); } } diff --git a/app/src/main/cpp/BrowserWorld.h b/app/src/main/cpp/BrowserWorld.h index ae3046bf6..e58d0909f 100644 --- a/app/src/main/cpp/BrowserWorld.h +++ b/app/src/main/cpp/BrowserWorld.h @@ -45,6 +45,7 @@ class BrowserWorld { void SetSurfaceTexture(const std::string& aName, jobject& aSurface); void AddWidget(int32_t aHandle, const WidgetPlacementPtr& placement); void UpdateWidget(int32_t aHandle, const WidgetPlacementPtr& aPlacement); + void UpdateWidgetRecursive(int32_t aHandle, const WidgetPlacementPtr& aPlacement); void RemoveWidget(int32_t aHandle); void StartWidgetResize(int32_t aHandle, const vrb::Vector& aMaxSize, const vrb::Vector& aMinSize); void FinishWidgetResize(int32_t aHandle); diff --git a/app/src/main/cpp/Controller.cpp b/app/src/main/cpp/Controller.cpp index 2c6e581ab..a5fb2dff7 100644 --- a/app/src/main/cpp/Controller.cpp +++ b/app/src/main/cpp/Controller.cpp @@ -58,6 +58,10 @@ Controller::operator=(const Controller& aController) { numButtons = aController.numButtons; memcpy(immersiveAxes, aController.immersiveAxes, sizeof(immersiveAxes)); numAxes = aController.numAxes; + numHaptics = aController.numHaptics; + inputFrameID = aController.inputFrameID; + pulseDuration = aController.pulseDuration; + pulseIntensity = aController.pulseIntensity; leftHanded = aController.leftHanded; inDeadZone = aController.inDeadZone; lastHoverEvent = aController.lastHoverEvent; @@ -91,6 +95,10 @@ Controller::Reset() { numButtons = 0; memset(immersiveAxes, 0, sizeof(immersiveAxes)); numAxes = 0; + numHaptics = 0; + inputFrameID = 0; + pulseDuration = 0.0f; + pulseIntensity = 0.0f; leftHanded = false; inDeadZone = true; lastHoverEvent = 0.0; diff --git a/app/src/main/cpp/Controller.h b/app/src/main/cpp/Controller.h index dafecd757..c311fa333 100644 --- a/app/src/main/cpp/Controller.h +++ b/app/src/main/cpp/Controller.h @@ -51,6 +51,11 @@ struct Controller { uint32_t numButtons; float immersiveAxes[kControllerMaxAxes]; uint32_t numAxes; + uint32_t numHaptics; + float inputFrameID; + float pulseDuration; + float pulseIntensity; + bool leftHanded; bool inDeadZone; double lastHoverEvent; diff --git a/app/src/main/cpp/ControllerContainer.cpp b/app/src/main/cpp/ControllerContainer.cpp index 34689766d..9b022a8f5 100644 --- a/app/src/main/cpp/ControllerContainer.cpp +++ b/app/src/main/cpp/ControllerContainer.cpp @@ -341,6 +341,45 @@ ControllerContainer::SetAxes(const int32_t aControllerIndex, const float* aData, } } +void +ControllerContainer::SetHapticCount(const int32_t aControllerIndex, const uint32_t aNumHaptics) { + if (!m.Contains(aControllerIndex)) { + return; + } + m.list[aControllerIndex].numHaptics = aNumHaptics; +} + +uint32_t +ControllerContainer::GetHapticCount(const int32_t aControllerIndex) { + if (!m.Contains(aControllerIndex)) { + return 0; + } + + return m.list[aControllerIndex].numHaptics; +} + +void +ControllerContainer::SetHapticFeedback(const int32_t aControllerIndex, const uint64_t aInputFrameID, + const float aPulseDuration, const float aPulseIntensity) { + if (!m.Contains(aControllerIndex)) { + return; + } + m.list[aControllerIndex].inputFrameID = aInputFrameID; + m.list[aControllerIndex].pulseDuration = aPulseDuration; + m.list[aControllerIndex].pulseIntensity = aPulseIntensity; +} + +void +ControllerContainer::GetHapticFeedback(const int32_t aControllerIndex, uint64_t & aInputFrameID, + float& aPulseDuration, float& aPulseIntensity) { + if (!m.Contains(aControllerIndex)) { + return; + } + aInputFrameID = m.list[aControllerIndex].inputFrameID; + aPulseDuration = m.list[aControllerIndex].pulseDuration; + aPulseIntensity = m.list[aControllerIndex].pulseIntensity; +} + void ControllerContainer::SetLeftHanded(const int32_t aControllerIndex, const bool aLeftHanded) { if (!m.Contains(aControllerIndex)) { diff --git a/app/src/main/cpp/ControllerContainer.h b/app/src/main/cpp/ControllerContainer.h index 7ca93f75c..c821e3471 100644 --- a/app/src/main/cpp/ControllerContainer.h +++ b/app/src/main/cpp/ControllerContainer.h @@ -43,6 +43,10 @@ class ControllerContainer : public crow::ControllerDelegate { void SetButtonCount(const int32_t aControllerIndex, const uint32_t aNumButtons) override; void SetButtonState(const int32_t aControllerIndex, const Button aWhichButton, const int32_t aImmersiveIndex, const bool aPressed, const bool aTouched, const float aImmersiveTrigger = -1.0f) override; void SetAxes(const int32_t aControllerIndex, const float* aData, const uint32_t aLength) override; + void SetHapticCount(const int32_t aControllerIndex, const uint32_t aNumHaptics) override; + uint32_t GetHapticCount(const int32_t aControllerIndex) override; + void SetHapticFeedback(const int32_t aControllerIndex, const uint64_t aInputFrameID, const float aPulseDuration, const float aPulseIntensity) override; + void GetHapticFeedback(const int32_t aControllerIndex, uint64_t &aInputFrameID, float& aPulseDuration, float& aPulseIntensity) override; void SetLeftHanded(const int32_t aControllerIndex, const bool aLeftHanded) override; void SetTouchPosition(const int32_t aControllerIndex, const float aTouchX, const float aTouchY) override; void EndTouch(const int32_t aControllerIndex) override; diff --git a/app/src/main/cpp/ControllerDelegate.h b/app/src/main/cpp/ControllerDelegate.h index e64e7c991..0e1243b04 100644 --- a/app/src/main/cpp/ControllerDelegate.h +++ b/app/src/main/cpp/ControllerDelegate.h @@ -42,6 +42,10 @@ class ControllerDelegate { virtual void SetButtonCount(const int32_t aControllerIndex, const uint32_t aNumButtons) = 0; virtual void SetButtonState(const int32_t aControllerIndex, const Button aWhichButton, const int32_t aImmersiveIndex, const bool aPressed, const bool aTouched, const float aImmersiveTrigger = -1.0f) = 0; virtual void SetAxes(const int32_t aControllerIndex, const float* aData, const uint32_t aLength) = 0; + virtual void SetHapticCount(const int32_t aControllerIndex, const uint32_t aNumHaptics) = 0; + virtual uint32_t GetHapticCount(const int32_t aControllerIndex) = 0; + virtual void SetHapticFeedback(const int32_t aControllerIndex, const uint64_t aInputFrameID, const float aPulseDuration, const float aPulseIntensity) = 0; + virtual void GetHapticFeedback(const int32_t aControllerIndex, uint64_t & aInputFrameID, float& aPulseDuration, float& aPulseIntensity) = 0; virtual void SetLeftHanded(const int32_t aControllerIndex, const bool aLeftHanded) = 0; virtual void SetTouchPosition(const int32_t aControllerIndex, const float aTouchX, const float aTouchY) = 0; virtual void EndTouch(const int32_t aControllerIndex) = 0; diff --git a/app/src/main/cpp/Device.h b/app/src/main/cpp/Device.h index 71911c02c..b02d84b89 100644 --- a/app/src/main/cpp/Device.h +++ b/app/src/main/cpp/Device.h @@ -19,6 +19,9 @@ const CapabilityFlags LinearAcceleration = 1u << 6u; const CapabilityFlags StageParameters = 1u << 7u; const CapabilityFlags MountDetection = 1u << 8u; const CapabilityFlags PositionEmulated = 1u << 9u; +const CapabilityFlags InlineSession = 1u << 10u; +const CapabilityFlags ImmersiveVRSession = 1u << 11u; +const CapabilityFlags ImmersiveARSession = 1u << 12u; enum class Eye { Left, Right }; enum class RenderMode { StandAlone, Immersive }; enum class CPULevel { Normal = 0, High }; diff --git a/app/src/main/cpp/ExternalVR.cpp b/app/src/main/cpp/ExternalVR.cpp index 540f837ca..0b7f9e0e6 100644 --- a/app/src/main/cpp/ExternalVR.cpp +++ b/app/src/main/cpp/ExternalVR.cpp @@ -254,6 +254,15 @@ ExternalVR::SetCapabilityFlags(const device::CapabilityFlags aFlags) { if (device::PositionEmulated & aFlags) { result |= static_cast(mozilla::gfx::VRDisplayCapabilityFlags::Cap_PositionEmulated); } + if (device::InlineSession & aFlags) { + result |= static_cast(mozilla::gfx::VRDisplayCapabilityFlags::Cap_Inline); + } + if (device::ImmersiveVRSession & aFlags) { + result |= static_cast(mozilla::gfx::VRDisplayCapabilityFlags::Cap_ImmersiveVR); + } + if (device::ImmersiveARSession & aFlags) { + result |= static_cast(mozilla::gfx::VRDisplayCapabilityFlags::Cap_ImmersiveAR); + } //m.deviceCapabilities = aFlags; m.system.displayState.capabilityFlags = static_cast(result); m.system.sensorState.flags = m.system.displayState.capabilityFlags; @@ -412,6 +421,7 @@ ExternalVR::PushFramePoses(const vrb::Matrix& aHeadTransform, const std::vector< for (int i = 0; i< controller.numAxes; ++i) { immersiveController.axisValue[i] = controller.immersiveAxes[i]; } + immersiveController.numHaptics = controller.numHaptics; immersiveController.hand = controller.leftHanded ? mozilla::gfx::ControllerHand::Left : mozilla::gfx::ControllerHand::Right; const uint16_t flags = GetControllerCapabilityFlags(controller.deviceCapabilities); @@ -489,6 +499,26 @@ ExternalVR::GetFrameResult(int32_t& aSurfaceHandle, int32_t& aTextureWidth, int3 aTextureHeight = (int32_t)m.browser.layerState[0].layer_stereo_immersive.textureSize.height; } +void +ExternalVR::SetHapticState(ControllerContainerPtr aControllerContainer) const { + const uint32_t count = aControllerContainer->GetControllerCount(); + uint32_t i = 0, j = 0; + for (i = 0; i < count; ++i) { + for (j = 0; j < mozilla::gfx::kVRHapticsMaxCount; ++j) { + if (m.browser.hapticState[j].controllerIndex == i && m.browser.hapticState[j].inputFrameID) { + aControllerContainer->SetHapticFeedback(i, m.browser.hapticState[j].inputFrameID, + m.browser.hapticState[j].pulseDuration + m.browser.hapticState[j].pulseStart, + m.browser.hapticState[j].pulseIntensity); + break; + } + } + // All hapticState has already been reset to zero, so it can't be match. + if (j == mozilla::gfx::kVRHapticsMaxCount) { + aControllerContainer->SetHapticFeedback(i, 0, 0.0f, 0.0f); + } + } +} + void ExternalVR::StopPresenting() { m.system.displayState.presentingGeneration++; diff --git a/app/src/main/cpp/ExternalVR.h b/app/src/main/cpp/ExternalVR.h index da66b8216..079df2df6 100644 --- a/app/src/main/cpp/ExternalVR.h +++ b/app/src/main/cpp/ExternalVR.h @@ -8,6 +8,7 @@ #include "vrb/MacroUtils.h" #include "Controller.h" +#include "ControllerContainer.h" #include "DeviceDelegate.h" #include "Device.h" #include @@ -59,6 +60,7 @@ class ExternalVR : public ImmersiveDisplay { int32_t& aTextureHeight, device::EyeRect& aLeftEye, device::EyeRect& aRightEye) const; + void SetHapticState(ControllerContainerPtr aControllerContainer) const; void StopPresenting(); void SetSourceBrowser(VRBrowserType aBrowser); ExternalVR(); diff --git a/app/src/main/cpp/moz_external_vr.h b/app/src/main/cpp/moz_external_vr.h index 3f78c6cc8..e2b48dbaa 100644 --- a/app/src/main/cpp/moz_external_vr.h +++ b/app/src/main/cpp/moz_external_vr.h @@ -47,8 +47,8 @@ namespace gfx { // and mapped files if we have both release and nightlies // running at the same time? Or...what if we have multiple // release builds running on same machine? (Bug 1563232) -#define SHMEM_VERSION "0.0.4" -static const int32_t kVRExternalVersion = 11; +#define SHMEM_VERSION "0.0.7" +static const int32_t kVRExternalVersion = 14; // We assign VR presentations to groups with a bitmask. // Currently, we will only display either content or chrome. @@ -63,6 +63,7 @@ static const uint32_t kVRGroupAll = 0xffffffff; static const int kVRDisplayNameMaxLen = 256; static const int kVRControllerNameMaxLen = 256; +static const int kProfileNameListMaxLen = 256; static const int kVRControllerMaxCount = 16; static const int kVRControllerMaxButtons = 64; static const int kVRControllerMaxAxis = 16; @@ -102,30 +103,38 @@ enum class ControllerCapabilityFlags : uint16_t { /** * Cap_Position is set if the Gamepad is capable of tracking its position. */ - Cap_Position = 1 << 1, + Cap_Position = 1 << 1, /** * Cap_Orientation is set if the Gamepad is capable of tracking its * orientation. */ - Cap_Orientation = 1 << 2, + Cap_Orientation = 1 << 2, /** * Cap_AngularAcceleration is set if the Gamepad is capable of tracking its * angular acceleration. */ - Cap_AngularAcceleration = 1 << 3, + Cap_AngularAcceleration = 1 << 3, /** * Cap_LinearAcceleration is set if the Gamepad is capable of tracking its * linear acceleration. */ - Cap_LinearAcceleration = 1 << 4, + Cap_LinearAcceleration = 1 << 4, + /** + * Cap_GripSpacePosition is set if the Gamepad has a grip space position. + */ + Cap_GripSpacePosition = 1 << 5, /** * Cap_All used for validity checking during IPC serialization */ - Cap_All = (1 << 5) - 1 + Cap_All = (1 << 6) - 1 }; #endif // ifndef MOZILLA_INTERNAL_API +enum class TargetRayMode : uint8_t { Gaze, TrackedPointer, Screen }; + +enum class GamepadMappingType : uint8_t { _empty, Standard, XRStandard }; + enum class VRDisplayBlendMode : uint8_t { Opaque, Additive, AlphaBlend }; enum class VRDisplayCapabilityFlags : uint16_t { @@ -133,12 +142,12 @@ enum class VRDisplayCapabilityFlags : uint16_t { /** * Cap_Position is set if the VRDisplay is capable of tracking its position. */ - Cap_Position = 1 << 1, + Cap_Position = 1 << 1, /** * Cap_Orientation is set if the VRDisplay is capable of tracking its * orientation. */ - Cap_Orientation = 1 << 2, + Cap_Orientation = 1 << 2, /** * Cap_Present is set if the VRDisplay is capable of presenting content to an * HMD or similar device. Can be used to indicate "magic window" devices that @@ -146,7 +155,7 @@ enum class VRDisplayCapabilityFlags : uint16_t { * meaningful. If false then calls to requestPresent should always fail, and * getEyeParameters should return null. */ - Cap_Present = 1 << 3, + Cap_Present = 1 << 3, /** * Cap_External is set if the VRDisplay is separate from the device's * primary display. If presenting VR content will obscure @@ -154,54 +163,54 @@ enum class VRDisplayCapabilityFlags : uint16_t { * un-set, the application should not attempt to mirror VR content * or update non-VR UI because that content will not be visible. */ - Cap_External = 1 << 4, + Cap_External = 1 << 4, /** * Cap_AngularAcceleration is set if the VRDisplay is capable of tracking its * angular acceleration. */ - Cap_AngularAcceleration = 1 << 5, + Cap_AngularAcceleration = 1 << 5, /** * Cap_LinearAcceleration is set if the VRDisplay is capable of tracking its * linear acceleration. */ - Cap_LinearAcceleration = 1 << 6, + Cap_LinearAcceleration = 1 << 6, /** * Cap_StageParameters is set if the VRDisplay is capable of room scale VR * and can report the StageParameters to describe the space. */ - Cap_StageParameters = 1 << 7, + Cap_StageParameters = 1 << 7, /** * Cap_MountDetection is set if the VRDisplay is capable of sensing when the * user is wearing the device. */ - Cap_MountDetection = 1 << 8, + Cap_MountDetection = 1 << 8, /** * Cap_PositionEmulated is set if the VRDisplay is capable of setting a * emulated position (e.g. neck model) even if still doesn't support 6DOF * tracking. */ - Cap_PositionEmulated = 1 << 9, + Cap_PositionEmulated = 1 << 9, /** * Cap_Inline is set if the device can be used for WebXR inline sessions * where the content is displayed within an element on the page. */ - Cap_Inline = 1 << 10, + Cap_Inline = 1 << 10, /** * Cap_ImmersiveVR is set if the device can give exclusive access to the * XR device display and that content is not intended to be integrated * with the user's environment */ - Cap_ImmersiveVR = 1 << 11, + Cap_ImmersiveVR = 1 << 11, /** * Cap_ImmersiveAR is set if the device can give exclusive access to the * XR device display and that content is intended to be integrated with * the user's environment. */ - Cap_ImmersiveAR = 1 << 12, + Cap_ImmersiveAR = 1 << 12, /** * Cap_All used for validity checking during IPC serialization */ - Cap_All = (1 << 13) - 1 + Cap_All = (1 << 13) - 1 }; #ifdef MOZILLA_INTERNAL_API @@ -332,6 +341,37 @@ struct VRControllerState { #else ControllerHand hand; #endif + // https://immersive-web.github.io/webxr/#enumdef-xrtargetraymode + TargetRayMode targetRayMode; + + // Space-delimited list of input profile names, in decending order + // of specificity. + // https://immersive-web.github.io/webxr/#dom-xrinputsource-profiles + char profiles[kProfileNameListMaxLen]; + + // https://immersive-web.github.io/webxr-gamepads-module/#enumdef-gamepadmappingtype + GamepadMappingType mappingType; + + // Start frame ID of the most recent primary select + // action, or 0 if the select action has never occurred. + uint64_t selectActionStartFrameId; + // End frame Id of the most recent primary select + // action, or 0 if action never occurred. + // If selectActionStopFrameId is less than + // selectActionStartFrameId, then the select + // action has not ended yet. + uint64_t selectActionStopFrameId; + + // start frame Id of the most recent primary squeeze + // action, or 0 if the squeeze action has never occurred. + uint64_t squeezeActionStartFrameId; + // End frame Id of the most recent primary squeeze + // action, or 0 if action never occurred. + // If squeezeActionStopFrameId is less than + // squeezeActionStartFrameId, then the squeeze + // action has not ended yet. + uint64_t squeezeActionStopFrameId; + uint32_t numButtons; uint32_t numAxes; uint32_t numHaptics; @@ -347,9 +387,20 @@ struct VRControllerState { #else ControllerCapabilityFlags flags; #endif + + // When Cap_Position is set in flags, pose corresponds + // to the controllers' pose in target ray space: + // https://immersive-web.github.io/webxr/#dom-xrinputsource-targetrayspace VRPose pose; + + // When Cap_GripSpacePosition is set in flags, gripPose corresponds + // to the controllers' pose in grip space: + // https://immersive-web.github.io/webxr/#dom-xrinputsource-gripspace + VRPose gripPose; + bool isPositionValid; bool isOrientationValid; + #ifdef MOZILLA_INTERNAL_API void Clear() { memset(this, 0, sizeof(VRControllerState)); } #endif @@ -421,6 +472,23 @@ struct VRBrowserState { #if defined(__ANDROID__) bool shutdown; #endif // defined(__ANDROID__) + /** + * In order to support WebXR's navigator.xr.IsSessionSupported call without + * displaying any permission dialogue, it is necessary to have a safe way to + * detect the capability of running a VR or AR session without activating XR + * runtimes or powering on hardware. + * + * API's such as OpenVR make no guarantee that hardware and software won't be + * left activated after enumerating devices, so each backend in gfx/vr/service + * must allow for more granular detection of capabilities. + * + * When detectRuntimesOnly is true, the initialization exits early after + * reporting the presence of XR runtime software. + * + * The result of the runtime detection is reported with the Cap_ImmersiveVR + * and Cap_ImmersiveAR bits in VRDisplayState.flags. + */ + bool detectRuntimesOnly; bool presentationActive; bool navigationTransitionActive; VRLayerState layerState[kVRLayerMaxCount]; @@ -475,6 +543,41 @@ struct VRWindowState { char signalName[32]; }; +enum class VRTelemetryId : uint8_t { + NONE = 0, + INSTALLED_FROM = 1, + ENTRY_METHOD = 2, + FIRST_RUN = 3, + TOTAL = 4, +}; + +enum class VRTelemetryInstallFrom: uint8_t { + User = 0, + FxR = 1, + HTC = 2, + Valve = 3, + TOTAL = 4, +}; + +enum class VRTelemetryEntryMethod: uint8_t { + SystemBtn = 0, + Library = 1, + Gaze = 2, + TOTAL = 3, +}; + +struct VRTelemetryState { + uint32_t uid; + + bool installedFrom : 1; + bool entryMethod : 1; + bool firstRun : 1; + + uint8_t installedFromValue : 3; + uint8_t entryMethodValue : 3; + bool firstRunValue : 1; +}; + struct VRExternalShmem { int32_t version; int32_t size; @@ -502,6 +605,7 @@ struct VRExternalShmem { #endif // !defined(__ANDROID__) #if defined(XP_WIN) VRWindowState windowState; + VRTelemetryState telemetryState; #endif #ifdef MOZILLA_INTERNAL_API void Clear() volatile { diff --git a/app/src/main/res/color/context_menu_icon_color.xml b/app/src/main/res/color/context_menu_icon_color.xml deleted file mode 100644 index abd65d1b1..000000000 --- a/app/src/main/res/color/context_menu_icon_color.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ 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 deleted file mode 100644 index abd65d1b1..000000000 --- a/app/src/main/res/color/context_menu_text_color.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/color/fog_void_tint.xml b/app/src/main/res/color/fog_void_tint.xml index 7bc860bce..8ddbd75ee 100644 --- a/app/src/main/res/color/fog_void_tint.xml +++ b/app/src/main/res/color/fog_void_tint.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/color/library_panel_button_text_color.xml b/app/src/main/res/color/library_panel_button_text_color.xml index daf8f8ebd..34a5813a7 100644 --- a/app/src/main/res/color/library_panel_button_text_color.xml +++ b/app/src/main/res/color/library_panel_button_text_color.xml @@ -1,6 +1,5 @@ - + diff --git a/app/src/main/res/color/library_panel_context_menu_item_color.xml b/app/src/main/res/color/library_panel_context_menu_item_color.xml index 7874cdfcf..c944e626d 100644 --- a/app/src/main/res/color/library_panel_context_menu_item_color.xml +++ b/app/src/main/res/color/library_panel_context_menu_item_color.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/color/library_panel_description_color.xml b/app/src/main/res/color/library_panel_description_color.xml index 07deb1020..863ea1a6e 100644 --- a/app/src/main/res/color/library_panel_description_color.xml +++ b/app/src/main/res/color/library_panel_description_color.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/color/library_panel_icon_color.xml b/app/src/main/res/color/library_panel_icon_color.xml index 1b8ba77f5..0d8615217 100644 --- a/app/src/main/res/color/library_panel_icon_color.xml +++ b/app/src/main/res/color/library_panel_icon_color.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/color/library_panel_title_text_color.xml b/app/src/main/res/color/library_panel_title_text_color.xml index 0c4b800c5..93a26e2d7 100644 --- a/app/src/main/res/color/library_panel_title_text_color.xml +++ b/app/src/main/res/color/library_panel_title_text_color.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/color/rhino_void_tint.xml b/app/src/main/res/color/rhino_void_tint.xml deleted file mode 100644 index 096117c90..000000000 --- a/app/src/main/res/color/rhino_void_tint.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/void_fog_tint.xml b/app/src/main/res/color/void_fog_tint.xml deleted file mode 100644 index 69ca800b7..000000000 --- a/app/src/main/res/color/void_fog_tint.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/autocompletion_item_active_color.xml b/app/src/main/res/drawable/autocompletion_item_active_color.xml index 7627665e0..5f9d4e672 100644 --- a/app/src/main/res/drawable/autocompletion_item_active_color.xml +++ b/app/src/main/res/drawable/autocompletion_item_active_color.xml @@ -1,5 +1,3 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/autocompletion_item_background.xml b/app/src/main/res/drawable/autocompletion_item_background.xml index 0f0e82ea2..7025a479f 100644 --- a/app/src/main/res/drawable/autocompletion_item_background.xml +++ b/app/src/main/res/drawable/autocompletion_item_background.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/autocompletion_item_background_first.xml b/app/src/main/res/drawable/autocompletion_item_background_first.xml index af7b2f4f0..ffb1fb13b 100644 --- a/app/src/main/res/drawable/autocompletion_item_background_first.xml +++ b/app/src/main/res/drawable/autocompletion_item_background_first.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/autocompletion_item_background_last.xml b/app/src/main/res/drawable/autocompletion_item_background_last.xml index 1a3b15ba0..1e8544bc4 100644 --- a/app/src/main/res/drawable/autocompletion_item_background_last.xml +++ b/app/src/main/res/drawable/autocompletion_item_background_last.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/checkbox.xml b/app/src/main/res/drawable/checkbox.xml index a3d03a68e..0b498cfc0 100644 --- a/app/src/main/res/drawable/checkbox.xml +++ b/app/src/main/res/drawable/checkbox.xml @@ -1,7 +1,5 @@ - + - + @@ -26,4 +24,4 @@ - \ No newline at end of file + \ 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 deleted file mode 100644 index 51687024e..000000000 --- a/app/src/main/res/drawable/context_menu_item_background_color.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_button_text_color.xml b/app/src/main/res/drawable/dialog_button_text_color.xml index 67c3365cb..e25a30918 100644 --- a/app/src/main/res/drawable/dialog_button_text_color.xml +++ b/app/src/main/res/drawable/dialog_button_text_color.xml @@ -1,7 +1,5 @@ - + - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/dialog_close_button_icon_color.xml b/app/src/main/res/drawable/dialog_close_button_icon_color.xml deleted file mode 100644 index c5c901e82..000000000 --- a/app/src/main/res/drawable/dialog_close_button_icon_color.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/dialog_highlighted_button_background.xml b/app/src/main/res/drawable/dialog_highlighted_button_background.xml index df106b119..0496c6c89 100644 --- a/app/src/main/res/drawable/dialog_highlighted_button_background.xml +++ b/app/src/main/res/drawable/dialog_highlighted_button_background.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/dialog_regular_button_background.xml b/app/src/main/res/drawable/dialog_regular_button_background.xml index 76599eeff..ebbe02690 100644 --- a/app/src/main/res/drawable/dialog_regular_button_background.xml +++ b/app/src/main/res/drawable/dialog_regular_button_background.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/edittext_textcolor.xml b/app/src/main/res/drawable/edittext_textcolor.xml index cf352e838..cfd780220 100644 --- a/app/src/main/res/drawable/edittext_textcolor.xml +++ b/app/src/main/res/drawable/edittext_textcolor.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/empty_drawable.xml b/app/src/main/res/drawable/empty_drawable.xml index 55453b8ac..b56e31ded 100644 --- a/app/src/main/res/drawable/empty_drawable.xml +++ b/app/src/main/res/drawable/empty_drawable.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/fast_scroll_thumb.xml b/app/src/main/res/drawable/fast_scroll_thumb.xml index ef7e25b2e..9ff908967 100644 --- a/app/src/main/res/drawable/fast_scroll_thumb.xml +++ b/app/src/main/res/drawable/fast_scroll_thumb.xml @@ -1,7 +1,5 @@ - + - + @@ -20,4 +18,4 @@ - + diff --git a/app/src/main/res/drawable/fullscreen_button_first.xml b/app/src/main/res/drawable/fullscreen_button_first.xml index b0e6d3a79..d42dd060e 100644 --- a/app/src/main/res/drawable/fullscreen_button_first.xml +++ b/app/src/main/res/drawable/fullscreen_button_first.xml @@ -1,7 +1,5 @@ - + @@ -46,4 +44,4 @@ android:bottomLeftRadius="20dp"/> - + diff --git a/app/src/main/res/drawable/fullscreen_button_last.xml b/app/src/main/res/drawable/fullscreen_button_last.xml index 0f2b47ece..1310497fe 100644 --- a/app/src/main/res/drawable/fullscreen_button_last.xml +++ b/app/src/main/res/drawable/fullscreen_button_last.xml @@ -1,7 +1,5 @@ - + @@ -46,4 +44,4 @@ android:bottomRightRadius="20dp"/> - + diff --git a/app/src/main/res/drawable/fullscreen_button_private.xml b/app/src/main/res/drawable/fullscreen_button_private.xml index d7ed4855c..6a4b68fc8 100644 --- a/app/src/main/res/drawable/fullscreen_button_private.xml +++ b/app/src/main/res/drawable/fullscreen_button_private.xml @@ -1,7 +1,5 @@ - + @@ -22,4 +20,4 @@ - + diff --git a/app/src/main/res/drawable/fullscreen_button_private_first.xml b/app/src/main/res/drawable/fullscreen_button_private_first.xml index 49e32ed75..a27b82c5d 100644 --- a/app/src/main/res/drawable/fullscreen_button_private_first.xml +++ b/app/src/main/res/drawable/fullscreen_button_private_first.xml @@ -1,7 +1,5 @@ - + @@ -42,4 +40,4 @@ android:bottomLeftRadius="20dp"/> - + diff --git a/app/src/main/res/drawable/fullscreen_button_private_last.xml b/app/src/main/res/drawable/fullscreen_button_private_last.xml index f2dc4b46f..9dc160b90 100644 --- a/app/src/main/res/drawable/fullscreen_button_private_last.xml +++ b/app/src/main/res/drawable/fullscreen_button_private_last.xml @@ -1,7 +1,5 @@ - + @@ -30,4 +28,4 @@ - + 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 deleted file mode 100644 index f9d08e062..000000000 --- a/app/src/main/res/drawable/ic_context_menu_new_window.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_icon_browser.xml b/app/src/main/res/drawable/ic_icon_browser.xml deleted file mode 100644 index eb6a7cbda..000000000 --- a/app/src/main/res/drawable/ic_icon_browser.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_icon_dialog_cancel.xml b/app/src/main/res/drawable/ic_icon_dialog_cancel.xml deleted file mode 100644 index 0253d7f25..000000000 --- a/app/src/main/res/drawable/ic_icon_dialog_cancel.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_icon_dialog_tabs_active.xml b/app/src/main/res/drawable/ic_icon_dialog_tabs_active.xml deleted file mode 100644 index 99ecee26e..000000000 --- a/app/src/main/res/drawable/ic_icon_dialog_tabs_active.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_icon_empty.xml b/app/src/main/res/drawable/ic_icon_empty.xml deleted file mode 100644 index 7fb20b222..000000000 --- a/app/src/main/res/drawable/ic_icon_empty.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_icon_language_add.xml b/app/src/main/res/drawable/ic_icon_language_add.xml index edcb72f55..5ca999837 100644 --- a/app/src/main/res/drawable/ic_icon_language_add.xml +++ b/app/src/main/res/drawable/ic_icon_language_add.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/ic_icon_language_delete.xml b/app/src/main/res/drawable/ic_icon_language_delete.xml index cf446f48d..412dee4cf 100644 --- a/app/src/main/res/drawable/ic_icon_language_delete.xml +++ b/app/src/main/res/drawable/ic_icon_language_delete.xml @@ -1,8 +1,6 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_icon_language_move_down.xml b/app/src/main/res/drawable/ic_icon_language_move_down.xml index 224e469c8..7b8832c12 100644 --- a/app/src/main/res/drawable/ic_icon_language_move_down.xml +++ b/app/src/main/res/drawable/ic_icon_language_move_down.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/ic_icon_language_move_up.xml b/app/src/main/res/drawable/ic_icon_language_move_up.xml index 44dbe6acf..07bdc2552 100644 --- a/app/src/main/res/drawable/ic_icon_language_move_up.xml +++ b/app/src/main/res/drawable/ic_icon_language_move_up.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/ic_icon_move_down.xml b/app/src/main/res/drawable/ic_icon_move_down.xml index 19281d661..f32ca761b 100644 --- a/app/src/main/res/drawable/ic_icon_move_down.xml +++ b/app/src/main/res/drawable/ic_icon_move_down.xml @@ -1,9 +1,11 @@ + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M20.1176,5.4475 L12,13.5475 3.8824,5.4475 1.3887,7.9412 12,18.5525 22.6113,7.9412Z" + android:strokeWidth="1.76854455" /> + diff --git a/app/src/main/res/drawable/ic_icon_move_up.xml b/app/src/main/res/drawable/ic_icon_move_up.xml index 4f16e43fa..ff5a1cc8d 100644 --- a/app/src/main/res/drawable/ic_icon_move_up.xml +++ b/app/src/main/res/drawable/ic_icon_move_up.xml @@ -1,9 +1,10 @@ - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_left_black_24px.xml b/app/src/main/res/drawable/ic_keyboard_arrow_left_black_24px.xml deleted file mode 100644 index 3867d83e6..000000000 --- a/app/src/main/res/drawable/ic_keyboard_arrow_left_black_24px.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_right_black_24px.xml b/app/src/main/res/drawable/ic_keyboard_arrow_right_black_24px.xml deleted file mode 100644 index a02d8320a..000000000 --- a/app/src/main/res/drawable/ic_keyboard_arrow_right_black_24px.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_keyboard_keyboard.xml b/app/src/main/res/drawable/ic_keyboard_keyboard.xml deleted file mode 100644 index 5dc167ac2..000000000 --- a/app/src/main/res/drawable/ic_keyboard_keyboard.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/keyboard_button_background.xml b/app/src/main/res/drawable/keyboard_button_background.xml index 4aff5fe82..564ea037e 100644 --- a/app/src/main/res/drawable/keyboard_button_background.xml +++ b/app/src/main/res/drawable/keyboard_button_background.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/lang_selector_button_background.xml b/app/src/main/res/drawable/lang_selector_button_background.xml index e4aa03a86..e872244a5 100644 --- a/app/src/main/res/drawable/lang_selector_button_background.xml +++ b/app/src/main/res/drawable/lang_selector_button_background.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/lang_selector_button_color.xml b/app/src/main/res/drawable/lang_selector_button_color.xml index 170015bb1..bdfb33755 100644 --- a/app/src/main/res/drawable/lang_selector_button_color.xml +++ b/app/src/main/res/drawable/lang_selector_button_color.xml @@ -1,6 +1,4 @@ - + - + @@ -20,4 +18,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/drawable/library_context_menu_item_background_middle.xml b/app/src/main/res/drawable/library_context_menu_item_background_middle.xml index b1a16b85a..0c7d42ab7 100644 --- a/app/src/main/res/drawable/library_context_menu_item_background_middle.xml +++ b/app/src/main/res/drawable/library_context_menu_item_background_middle.xml @@ -1,7 +1,5 @@ - + @@ -17,4 +15,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/drawable/library_context_menu_item_background_single.xml b/app/src/main/res/drawable/library_context_menu_item_background_single.xml index 4ab39bbd7..7c243ee02 100644 --- a/app/src/main/res/drawable/library_context_menu_item_background_single.xml +++ b/app/src/main/res/drawable/library_context_menu_item_background_single.xml @@ -1,7 +1,5 @@ - + @@ -20,4 +18,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/drawable/library_context_menu_item_background_top.xml b/app/src/main/res/drawable/library_context_menu_item_background_top.xml index e8ef4c799..93fbfdd02 100644 --- a/app/src/main/res/drawable/library_context_menu_item_background_top.xml +++ b/app/src/main/res/drawable/library_context_menu_item_background_top.xml @@ -1,7 +1,5 @@ - + @@ -20,4 +18,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/drawable/library_item_background_color.xml b/app/src/main/res/drawable/library_item_background_color.xml index 4dc6d05c8..7835fe288 100644 --- a/app/src/main/res/drawable/library_item_background_color.xml +++ b/app/src/main/res/drawable/library_item_background_color.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/main_button.xml b/app/src/main/res/drawable/main_button.xml index b9c9177a0..5eac9751e 100644 --- a/app/src/main/res/drawable/main_button.xml +++ b/app/src/main/res/drawable/main_button.xml @@ -1,7 +1,5 @@ - + @@ -14,13 +12,13 @@ - + - + diff --git a/app/src/main/res/drawable/main_button_icon_color.xml b/app/src/main/res/drawable/main_button_icon_color.xml index c5c901e82..af1298c81 100644 --- a/app/src/main/res/drawable/main_button_icon_color.xml +++ b/app/src/main/res/drawable/main_button_icon_color.xml @@ -1,6 +1,4 @@ - + + + + - + @@ -14,13 +12,13 @@ - + - + diff --git a/app/src/main/res/drawable/main_button_text_color.xml b/app/src/main/res/drawable/main_button_text_color.xml index e075e7adf..c13acc9ac 100644 --- a/app/src/main/res/drawable/main_button_text_color.xml +++ b/app/src/main/res/drawable/main_button_text_color.xml @@ -1,6 +1,4 @@ - + - + diff --git a/app/src/main/res/drawable/media_seekbar_thumb.xml b/app/src/main/res/drawable/media_seekbar_thumb.xml index 8b9edcd6b..2c3f670db 100644 --- a/app/src/main/res/drawable/media_seekbar_thumb.xml +++ b/app/src/main/res/drawable/media_seekbar_thumb.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/menu_item_background.xml b/app/src/main/res/drawable/menu_item_background.xml index 6c6a3af90..ae8c5b2bd 100644 --- a/app/src/main/res/drawable/menu_item_background.xml +++ b/app/src/main/res/drawable/menu_item_background.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/menu_item_background_first.xml b/app/src/main/res/drawable/menu_item_background_first.xml index b9d687151..d29b41d7f 100644 --- a/app/src/main/res/drawable/menu_item_background_first.xml +++ b/app/src/main/res/drawable/menu_item_background_first.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/menu_item_background_last.xml b/app/src/main/res/drawable/menu_item_background_last.xml index a4d29cfbf..718283cce 100644 --- a/app/src/main/res/drawable/menu_item_background_last.xml +++ b/app/src/main/res/drawable/menu_item_background_last.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/menu_item_background_single.xml b/app/src/main/res/drawable/menu_item_background_single.xml index 43f1f7505..7480fd2b6 100644 --- a/app/src/main/res/drawable/menu_item_background_single.xml +++ b/app/src/main/res/drawable/menu_item_background_single.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/menu_item_color.xml b/app/src/main/res/drawable/menu_item_color.xml index d306a0f9e..253d1fe39 100644 --- a/app/src/main/res/drawable/menu_item_color.xml +++ b/app/src/main/res/drawable/menu_item_color.xml @@ -1,7 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/microphone_icon_background.xml b/app/src/main/res/drawable/microphone_icon_background.xml deleted file mode 100644 index 71c7f59b1..000000000 --- a/app/src/main/res/drawable/microphone_icon_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/navigation_button_color.xml b/app/src/main/res/drawable/navigation_button_color.xml deleted file mode 100644 index d9ed0e8b2..000000000 --- a/app/src/main/res/drawable/navigation_button_color.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/panel_button_background.xml b/app/src/main/res/drawable/panel_button_background.xml deleted file mode 100644 index 76f3063cd..000000000 --- a/app/src/main/res/drawable/panel_button_background.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/prompt_button_background.xml b/app/src/main/res/drawable/prompt_button_background.xml index ac6759f7e..28e06fa5a 100644 --- a/app/src/main/res/drawable/prompt_button_background.xml +++ b/app/src/main/res/drawable/prompt_button_background.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/prompt_button_text_color.xml b/app/src/main/res/drawable/prompt_button_text_color.xml index 98adb7c34..123067ff4 100644 --- a/app/src/main/res/drawable/prompt_button_text_color.xml +++ b/app/src/main/res/drawable/prompt_button_text_color.xml @@ -1,7 +1,5 @@ - + - + diff --git a/app/src/main/res/drawable/prompt_item_text_color.xml b/app/src/main/res/drawable/prompt_item_text_color.xml index 98d47b968..20c3b800d 100644 --- a/app/src/main/res/drawable/prompt_item_text_color.xml +++ b/app/src/main/res/drawable/prompt_item_text_color.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/radio.xml b/app/src/main/res/drawable/radio.xml index 7f9fe5fdb..6085aaa7c 100644 --- a/app/src/main/res/drawable/radio.xml +++ b/app/src/main/res/drawable/radio.xml @@ -1,7 +1,5 @@ - + - + - + - + - + - + - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/rectangle_button_background.xml b/app/src/main/res/drawable/rectangle_button_background.xml index bde969a93..a78be9e32 100644 --- a/app/src/main/res/drawable/rectangle_button_background.xml +++ b/app/src/main/res/drawable/rectangle_button_background.xml @@ -1,7 +1,5 @@ - + @@ -26,4 +24,4 @@ - + diff --git a/app/src/main/res/drawable/selection_menu_background.xml b/app/src/main/res/drawable/selection_menu_background.xml index fab616db3..831afeb11 100644 --- a/app/src/main/res/drawable/selection_menu_background.xml +++ b/app/src/main/res/drawable/selection_menu_background.xml @@ -3,5 +3,6 @@ android:shape="rectangle"> + \ No newline at end of file diff --git a/app/src/main/res/drawable/selection_menu_background_triangle.xml b/app/src/main/res/drawable/selection_menu_background_triangle.xml index 916f45f60..3bba8c3d3 100644 --- a/app/src/main/res/drawable/selection_menu_background_triangle.xml +++ b/app/src/main/res/drawable/selection_menu_background_triangle.xml @@ -9,22 +9,22 @@ android:name="trianglegroup" android:pivotX="50" android:pivotY="50" - android:scaleX="0.95" - android:scaleY="0.95" + android:scaleX="0.92" + android:scaleY="0.92" android:translateY="-50" android:rotation="180"> + android:strokeColor="@color/iron" + android:strokeWidth="6"/> - + diff --git a/app/src/main/res/drawable/settings_honeycomb_background.xml b/app/src/main/res/drawable/settings_honeycomb_background.xml index 016ce1e12..4579cee7d 100644 --- a/app/src/main/res/drawable/settings_honeycomb_background.xml +++ b/app/src/main/res/drawable/settings_honeycomb_background.xml @@ -1,7 +1,5 @@ - + - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/suggestion_background_color.xml b/app/src/main/res/drawable/suggestion_background_color.xml index 40ac6d216..957a1395c 100644 --- a/app/src/main/res/drawable/suggestion_background_color.xml +++ b/app/src/main/res/drawable/suggestion_background_color.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/suggestion_description_color.xml b/app/src/main/res/drawable/suggestion_description_color.xml index 0c39d1180..577450005 100644 --- a/app/src/main/res/drawable/suggestion_description_color.xml +++ b/app/src/main/res/drawable/suggestion_description_color.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/suggestion_icon_color.xml b/app/src/main/res/drawable/suggestion_icon_color.xml index e35e82146..0eac69513 100644 --- a/app/src/main/res/drawable/suggestion_icon_color.xml +++ b/app/src/main/res/drawable/suggestion_icon_color.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/suggestion_text_color.xml b/app/src/main/res/drawable/suggestion_text_color.xml index b9dc27e84..5ec95bdf3 100644 --- a/app/src/main/res/drawable/suggestion_text_color.xml +++ b/app/src/main/res/drawable/suggestion_text_color.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/switch_thumb.xml b/app/src/main/res/drawable/switch_thumb.xml index 0029ed72c..f8442af02 100644 --- a/app/src/main/res/drawable/switch_thumb.xml +++ b/app/src/main/res/drawable/switch_thumb.xml @@ -1,7 +1,5 @@ - + - + diff --git a/app/src/main/res/drawable/text_button.xml b/app/src/main/res/drawable/text_button.xml index 1ba2f2622..29fcc68c3 100644 --- a/app/src/main/res/drawable/text_button.xml +++ b/app/src/main/res/drawable/text_button.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/text_button_private.xml b/app/src/main/res/drawable/text_button_private.xml index bfd809e63..0ae770fc7 100644 --- a/app/src/main/res/drawable/text_button_private.xml +++ b/app/src/main/res/drawable/text_button_private.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/title_bar_background.xml b/app/src/main/res/drawable/title_bar_background.xml index f83cfb45d..516f3cbc8 100644 --- a/app/src/main/res/drawable/title_bar_background.xml +++ b/app/src/main/res/drawable/title_bar_background.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/title_bar_background_private.xml b/app/src/main/res/drawable/title_bar_background_private.xml index 475866940..4912daf8b 100644 --- a/app/src/main/res/drawable/title_bar_background_private.xml +++ b/app/src/main/res/drawable/title_bar_background_private.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/url_background.xml b/app/src/main/res/drawable/url_background.xml index f21280ec8..e1d0e06b5 100644 --- a/app/src/main/res/drawable/url_background.xml +++ b/app/src/main/res/drawable/url_background.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/url_background_outline.xml b/app/src/main/res/drawable/url_background_outline.xml index c3ee73b8e..4b9065198 100644 --- a/app/src/main/res/drawable/url_background_outline.xml +++ b/app/src/main/res/drawable/url_background_outline.xml @@ -38,6 +38,22 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/url_background_private.xml b/app/src/main/res/drawable/url_background_private.xml index 33ed3953b..f21f8ff89 100644 --- a/app/src/main/res/drawable/url_background_private.xml +++ b/app/src/main/res/drawable/url_background_private.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/url_background_private_outline.xml b/app/src/main/res/drawable/url_background_private_outline.xml index d1b461fb7..823b4acf1 100644 --- a/app/src/main/res/drawable/url_background_private_outline.xml +++ b/app/src/main/res/drawable/url_background_private_outline.xml @@ -38,6 +38,22 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/url_button.xml b/app/src/main/res/drawable/url_button.xml index 43dd8ba89..31b43387a 100644 --- a/app/src/main/res/drawable/url_button.xml +++ b/app/src/main/res/drawable/url_button.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/url_button_end.xml b/app/src/main/res/drawable/url_button_end.xml index 75d10af5c..4360f6f1c 100644 --- a/app/src/main/res/drawable/url_button_end.xml +++ b/app/src/main/res/drawable/url_button_end.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/url_button_end_private.xml b/app/src/main/res/drawable/url_button_end_private.xml index 90c917a7d..bd498eace 100644 --- a/app/src/main/res/drawable/url_button_end_private.xml +++ b/app/src/main/res/drawable/url_button_end_private.xml @@ -1,7 +1,5 @@ - + @@ -12,12 +10,14 @@ + + diff --git a/app/src/main/res/drawable/url_button_icon_color.xml b/app/src/main/res/drawable/url_button_icon_color.xml deleted file mode 100644 index c5c901e82..000000000 --- a/app/src/main/res/drawable/url_button_icon_color.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/url_button_private.xml b/app/src/main/res/drawable/url_button_private.xml index 8f152518a..583b741fd 100644 --- a/app/src/main/res/drawable/url_button_private.xml +++ b/app/src/main/res/drawable/url_button_private.xml @@ -1,7 +1,5 @@ - + @@ -9,12 +7,14 @@ - + + - + + diff --git a/app/src/main/res/drawable/url_button_start.xml b/app/src/main/res/drawable/url_button_start.xml index e70e59d92..985471d55 100644 --- a/app/src/main/res/drawable/url_button_start.xml +++ b/app/src/main/res/drawable/url_button_start.xml @@ -1,7 +1,5 @@ - + diff --git a/app/src/main/res/drawable/url_button_start_private.xml b/app/src/main/res/drawable/url_button_start_private.xml index eb3d6aaaa..c59702a8b 100644 --- a/app/src/main/res/drawable/url_button_start_private.xml +++ b/app/src/main/res/drawable/url_button_start_private.xml @@ -1,7 +1,5 @@ - + @@ -12,12 +10,14 @@ + + diff --git a/app/src/main/res/layout/base_app_dialog.xml b/app/src/main/res/layout/base_app_dialog.xml deleted file mode 100644 index bf1c4e30d..000000000 --- a/app/src/main/res/layout/base_app_dialog.xml +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + +
+ +
diff --git a/app/src/main/res/raw/error_style.css b/app/src/main/res/raw/error_style.css index 8771a3139..e99b22b4a 100644 --- a/app/src/main/res/raw/error_style.css +++ b/app/src/main/res/raw/error_style.css @@ -1,141 +1,157 @@ -:root { - --color-steel: #434c58; - --color-driftwood: #f4f4f4; - --color-dusk: #556f8e; - --color-void: #232426; - --color-text: #fff; -} - -*, -*:before, -*:after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -* { - font: 20px/1.2em BlinkMacSystemFont, -apple-system, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; -} +/* 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/. */ html, body { - align-items: center; - background: var(--color-driftwood); - display: grid; - grid-template-rows: auto; - height: 100%; - justify-items: center; - width: 100%; + margin: 0; + padding: 0; + height: 100%; + --moz-vertical-spacing: 10px; + --moz-background-height: 32px; } -h1 { - font-size: 2em; - font-weight: bold; - margin: 0 0 0.5em; - position: relative; -} - -strong { - font-weight: bold; -} +body { + background-size: 64px var(--moz-background-height); + /* background-size: 64px 32px; */ + background-repeat: repeat-x; -ol, -ul { - list-style-position: inside; - margin: 1em 0; -} + background-color: #363B40; + color: #FFFFFF; + padding: 0 20px; -ol, -ul, -p { - margin: 1em 0; + font-weight: 300; + font-size: 13px; + -moz-text-size-adjust: none; + font-family: sans-serif; } -ol:first-child, -ul:first-child, -p:first-child { - margin-top: 0; +ul { + /* Shove the list indicator so that its left aligned, but use outside so that text + * doesn't don't wrap the text around it */ + padding: 0 1em; + margin: 0; + list-style: round outside none; } -ol:last-child, -ul:last-child, -p:last-child { - margin-bottom: 0; +#errorShortDesc, +li:not(:last-of-type) { + /* Margins between the li and buttons below it won't be collapsed. Remove the bottom margin here. */ + margin: var(--moz-vertical-spacing) 0; } -ol { - list-style-type: decimal; +h1 { + margin: 0; + /* Since this has an underline, use padding for vertical spacing rather than margin */ + padding: var(--moz-vertical-spacing) 0; + font-weight: 300; + border-bottom: 1px solid #e0e2e5; } -li { - margin: 0.6em 0; - padding-left: 1em; - text-indent: -1em; +h2 { + font-size: small; + padding: 0; + margin: var(--moz-vertical-spacing) 0; } p { - line-height: 1.5em; -} - -a { - color: var(--color-dusk); - font-size: inherit; + margin: var(--moz-vertical-spacing) 0; } button { - background: var(--color-steel); - border-radius: 10px; - border: 2px solid var(--color-steel); - color: var(--color-text); - cursor: pointer; - font-family: inherit; - font-size: inherit; - font-weight: 900; - height: 3em; - padding: 0 1em; + /* Force buttons to display: block here to try and enfoce collapsing margins */ + display: block; + width: 100%; + border: none; + padding: 1rem; + font-family: sans-serif; + background-color: #00A4DC; + color: #FFFFFF; + font-weight: 300; + border-radius: 2px; + background-image: none; + margin: var(--moz-vertical-spacing) 0 0; +} + +.buttonSecondary{ + /* Force buttons to display: block here to try and enforce collapsing margins */ + display: block; + width: 100%; + border: none; + padding: 1rem; + font-family: sans-serif; + background-color: rgba(249, 249, 250, 0.1); + color: #FFFFFF; + font-weight: 300; + border-radius: 2px; + background-image: none; + margin: var(--moz-vertical-spacing) 0 0; } -button:hover { - background: transparent; - color: var(--color-steel); -} +#errorPageContainer { + /* If the page is greater than 550px center the content. + * This number should be kept in sync with the media query for tablets below */ + max-width: 550px; + margin: 0 auto; + transform: translateY(var(--moz-background-height)); + padding-bottom: var(--moz-vertical-spacing); -button:active { - background: var(--color-void); - border-color: var(--color-void); - color: var(--color-text); + min-height: calc(100% - var(--moz-background-height) - var(--moz-vertical-spacing)); + display: flex; + flex-direction: column; } -#errorPageContainer { - grid-column-start: 1; - width: 75%; +/* On large screen devices (hopefully a 7+ inch tablet, we already center content (see #errorPageContainer above). + Apply tablet specific styles here */ +@media (min-width: 550px) { + button { + min-width: 160px; + width: auto; + } + + /* If the tablet is tall as well, add some padding to make content feel a bit more centered */ + @media (min-height: 550px) { + #errorPageContainer { + padding-top: 64px; + min-height: calc(100% - 64px); + } + } } -#securityOverrideContent { - background: var(--color-text); - border: 2px solid var(--color-steel); - padding: 1em; +#advancedPanelButtonContainer { + background-color: rgba(128, 128, 147, 0.1); + display: flex; + justify-content: center; + padding: 0.5em; } -#errorIcon { - background-repeat: no-repeat; - background-position: 50% 50%; - height: 50px; - width: 50px; +#advancedPanelContainer { + width: 100%; + left: 0; } -[data-error='dnsNotFound'] #errorIcon { - background-image: url(../images/icon-errorpage-servernotfound.svg); +.advanced-panel { + display: none; + background-color: #202023; + border: 1px solid rgba(249, 249, 250, 0.2); + margin: 48px auto; + min-width: 13em; + max-width: 52em; } -[data-error='nssBadCert'] body, -[data-error='nssFailure2'] body { - background-image: url(../images/icon-errorpage-thisconnectionisuntrusted.svg); - background-repeat: repeat-x; +.button-container { + display: flex; + flex-flow: row; } -[data-error='nssBadCert'] #errorIcon, -[data-error='nssFailure2'] #errorIcon { - background-image: url(../images/icon-errorpage-secureconnectionfailed.svg); +.button-container > button { + flex: 1 0 50%; + text-align: center; + margin: 0em 0.1em 0em 0.1em; } + +#badCertTechnicalInfo { + margin: 0em 1em 1em; + overflow: auto; + white-space: pre-line; +} \ No newline at end of file diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index c1ef4daa6..83ca35a16 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -124,6 +124,19 @@ opens a dialog box that contains settings that an application or Web developer might want to change. --> Indstillinger for udviklere + + Log in på konto + + + Håndter konto + + + Log ud + + + Genopret forbindelse + Sprog @@ -274,6 +287,16 @@ a the survey web site in opened in the currently focused window --> Send din feedback + + Log ind + + + Log ud + + + Genopret forbindelse + Genstart påkrævet @@ -325,6 +348,12 @@ --> Aktiver debug-logging + + Aktivér UI-hardware-acceleration + Aktiver Servo @@ -473,10 +502,67 @@ for the home page. --> Startside + + Konto + + + Indstillinger for synkronisering + + + Synkroniser nu + + + Synkroniserer... + + + Vælg hvad du vil synkronisere på dine enheder med Firefox + + + Bogmærker + + + Historik + + + Nulstil + + + Log ind på din konto for at synkronisere + + + Indstillinger for synkronisering + + + Senest synkroniseret lige nu + + + Ikke synkroniseret endnu + + + Synkronisering er slået fra + + + Synkroniserings-fejl + + + Senest synkroniseret for %1$d minutter siden + Afspil DRM-kontrolleret indhold (<a href="https://da.wikipedia.org/wiki/Digital_Rights_Management">Læs mere</a>) + + Gendan faneblade og vinduer efter genstart + Beskyttelse mod sporing @@ -516,6 +602,10 @@ of this storage even though the storage may not reside on external removable media. --> Læs eksternt lager + + Kamera og mikrofon + Bloker pop op-vinduer @@ -546,6 +636,14 @@ Fra + + Synkroniser + + + Synkroniser ikke + Hvad vil du søge efter på nettet? @@ -570,6 +668,9 @@ device's microphone. It explains why Voice Search is not working. --> Giv adgang til mikrofonen for at bruge stemme-søgning. + + Luk + Søg på nettet eller indtast en adresse @@ -682,6 +783,21 @@ Indlæser bogmærker + + Mobil-bogmærker + + + Skrivebords-bogmærker + + + Bogmærke-menuen + + + Bogmærkelinje + + + Andre bogmærker + @@ -710,6 +826,10 @@ all the bookmarks. --> Vis alle bogmærker + + Synkroniser + Historik @@ -738,6 +858,31 @@ clearing history is displayed. --> Ryd historik + + Synkroniser + + + Synkroniserer… + + + Vil du logge ud fra din Firefox-konto? + + + Når du logger ud, kan du ikke sende eller modtage faneblade fra andre enheder. Dine bogmærker og din historik vil heller ikke blive synkroniseret længere. + + + Ryd historik og bogmærker fra denne enhed + + + Log ud + + + Fortryd + Annuller @@ -770,6 +915,10 @@ the more button is pressed in a history item. It opens the selected history item in a new window. --> Åbn i et nyt vindue + + Åbn i et nyt faneblad + Føj til bogmærker @@ -793,6 +942,14 @@ Følgende websteder har bedt om lov til at åbne pop op-vinduer: + + Når du åbner et websted med pop op-vinduer, vil du se en dialogboks, der giver dig mulighed for at blokere pop op-vinduer. + + + En liste over websteder med tilladelser vil blive vist her. + Nulstil controller-indstillinger @@ -885,6 +1042,10 @@ browser's navigation bar. The button it labels, when pressed, clears the navigation bar text. --> Ryd + + Pop op-vindue blokeret + Vis info om websted. @@ -906,10 +1067,10 @@ Afslut privat browsing - Åbn i et nyt vindue. + Åbn i et nyt vindue - Åbn i et nyt faneblad. + Åbn i et nyt faneblad Kopier link @@ -1077,7 +1238,7 @@ - Vis ikke + Annuller @@ -1132,4 +1293,77 @@ the Select` button. When clicked it closes all the previously selected tabs --> Nyt faneblad tilføjet! + + + Faneblad sendt! + + + Send faneblad til enhed + + + Tilpas vinduesstørrelse + + + Skrivebords-tilstand + + + Menu + + + Send faneblad til enhed + + + Vælg en enhed for at sende fanebladet. + + + Tilgængelige enheder: + + + Ingen enheder forbundet. + + + Synkroniserer… + + + Send + + + Log ind for at sende faneblade + + + Log ind eller opret en ny Firefox-konto for at sende faneblade fra Firefox på din computer eller din mobile enhed til dit headset. + + + Med en Firefox-konto kan du nemt synkronisere bogmærker og historik mellem alle dine enheder. + + + Log ind + + + Gå i gang + + + + Langsomt script + + + En webside får din browser til at være langsom (%1$s). Hvad vil du gøre? + + + Stop siden + + + Vent diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 422cba95f..dbf385d6d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -348,6 +348,12 @@ --> Debug-Protokollierung aktivieren + + UI-Hardwarebeschleunigung aktivieren + Servo aktivieren @@ -552,6 +558,10 @@ and is used to enable or disable playback of DRM controlled content. --> DRM-kontrollierte Inhalte wiedergeben (<a href="http://somesite.com/">Weitere Informationen</a>) + + Tabs und Fenster nach Neustart wiederherstellen + Schutz vor Aktivitätenverfolgung @@ -591,6 +601,10 @@ of this storage even though the storage may not reside on external removable media. --> Externen Speicher lesen + + Kamera und Mikrofon + Pop-up-Fenster blockieren @@ -654,6 +668,9 @@ device's microphone. It explains why Voice Search is not working. --> Bitte erlauben Sie den Zugriff auf das Mikrofon, um die Sprachsuche zu verwenden. + + Schließen + Das Web durchsuchen oder eine Adresse eingeben @@ -848,6 +865,24 @@ Wird synchronisiert… + + Vom Firefox-Konto abmelden? + + + Wenn Sie sich abmelden, können Sie keine Tabs von anderen Geräten senden oder empfangen. Ihre Lesezeichen und die Chronik werden ebenfalls nicht mehr synchronisiert. + + + Chronik und Lesezeichen von diesem Gerät löschen + + + Abmelden + + + Abbrechen + Abbrechen @@ -907,6 +942,14 @@ Die folgenden Websites bitten um die Erlaubnis, Pop-up-Fenster zu öffnen: + + Wenn Sie eine Website mit Pop-up-Fenstern öffnen, wird ein Dialogfeld angezeigt, in dem Sie auswählen können, ob Pop-up-Fenster blockiert werden sollen. + + + Eine Liste der Websites mit Berechtigungen wird hier angezeigt. + Controller-Einstellungen zurücksetzen diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index b0b2d20b6..5a77c41c9 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -124,6 +124,19 @@ opens a dialog box that contains settings that an application or Web developer might want to change. --> Opciones de desarrollador + + Iniciar sesión en cuenta + + + Administrar cuenta + + + Cerrar sesión + + + Volver a conectar + Idioma @@ -274,6 +287,16 @@ a the survey web site in opened in the currently focused window --> Envíanos tus comentarios + + Iniciar sesión + + + Cerrar sesión + + + Volver a conectar + Se requiere reiniciar @@ -325,6 +348,12 @@ --> Habilitar registro de depuración + + Activar aceleración de hardware para la interfaz de usuario + Habilitar Servo @@ -472,10 +501,67 @@ for the home page. --> Página de inicio + + Cuenta + + + Configuración de sincronización + + + Sincronizar ahora + + + Sincronizando... + + + Elije qué sincronizar en tus dispositivos con Firefox + + + Marcadores + + + Historial + + + Restablecer + + + Iniciar sesión en la cuenta para sincronizar + + + Configuración de sincronización + + + Última sincronización: ahora + + + Aún no sincronizado + + + La sincronización está desactivada + + + Error en la sincronización + + + Última sincronización hace %1$d minutos + Reproducir contenido controlado por DRM (<a href="http://somesite.com/">Saber más</a>) + + Restaurar pestañas y ventanas después de reiniciar + Protección contra rastreo @@ -515,6 +601,10 @@ of this storage even though the storage may not reside on external removable media. --> Leer almacenamiento externo + + Cámara y micrófono + Bloquear ventanas emergentes @@ -547,6 +637,14 @@ Desactivado + + Sincronizar + + + No sincronizar + ¿Qué te gustaría buscar en la Web? @@ -571,6 +669,9 @@ device's microphone. It explains why Voice Search is not working. --> Permita el acceso al micrófono para usar la búsqueda por voz. + + Cerrar + Busca en la Web o escribe la dirección @@ -683,6 +784,21 @@ Cargando marcadores + + Marcadores del móvil + + + Marcadores de escritorio + + + Menú de marcadores + + + Barra de herramientas de marcadores + + + Otros marcadores + @@ -711,6 +827,10 @@ all the bookmarks. --> Mostrar todos los marcadores + + Sincronizar + Historial @@ -739,6 +859,31 @@ clearing history is displayed. --> Limpiar historial + + Sincronizar + + + Sincronizando… + + + ¿Salir de tu cuenta Firefox? + + + Cuando cierres la sesión, no podrás enviar ni recibir pestañas de otros dispositivos. Tus marcadores e historial también dejarán de sincronizarse. + + + Borrar el historial y marcadores de este dispositivo + + + Cerrar sesión + + + Cancelar + Cancelar @@ -771,6 +916,10 @@ the more button is pressed in a history item. It opens the selected history item in a new window. --> Abrir en una ventana nueva + + Abrir en una pestaña nueva + Añadir a Marcadores @@ -794,6 +943,14 @@ Los siguientes sitios web han solicitado abrir ventanas emergentes: + + Cuando abres un sitio web con ventanas emergentes, verás un cuadro de diálogo que te permite bloquear ventanas emergentes. + + + Una lista de sitios con permisos aparecerá aquí. + Restablecer ajustes del controlador @@ -886,6 +1043,10 @@ browser's navigation bar. The button it labels, when pressed, clears the navigation bar text. --> Limpiar + + Ventana emergente bloqueada + Mostrar información del sitio. @@ -907,10 +1068,10 @@ Salir de la navegación privada - Abrir en una nueva ventana. + Abrir en una nueva ventana - Abrir en una nueva pestaña. + Abrir en una nueva pestaña Copiar enlace @@ -1078,7 +1239,7 @@ - No mostrar + Cancelar @@ -1133,4 +1294,78 @@ the Select` button. When clicked it closes all the previously selected tabs --> ¡Nueva pestaña añadida! + + + + ¡Pestaña enviada! + + + Enviar pestaña al dispositivo + + + Cambiar tamaño de ventana + + + Modo de escritorio + + + Menú + + + Enviar pestaña al dispositivo + + + Elije un dispositivo para enviar la pestaña. + + + Dispositivos disponibles: + + + No hay dispositivos conectados. + + + Sincronizando... + + + Enviar + + + Inicia sesión para enviar pestañas + + + Inicia sesión o crea una nueva cuenta de Firefox para enviar pestañas desde Firefox en tu ordenador de escritorio o dispositivo móvil a tus auriculares. + + + La cuenta de Firefox también te permite sincronizar marcadores e historial en todos tus dispositivos. + + + Iniciar sesión + + + Comenzar a navegar + + + + Script lento + + + Una página web está ralentizando tu navegador (%1$s). ¿Qué te gustaría hacer? + + + Detenerlo + + + Esperar diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 8ba84cdbe..0c673fb6a 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -89,11 +89,6 @@ sdcard. '%1$s' will be replaced at runtime with the website's domain name. --> ¿Desea permitir que %1$s guarde datos en el almacenamiento persistente? - - versión %1$s - @@ -111,6 +106,20 @@ describes Mozilla's Privacy Policy. --> Política de privacidad + + Permisos de sitios para ventanas emergentes + + + Puedes especificar qué sitios web tienen permitido abrir ventanas emergentes + + + Permitir + + + Bloquear + Opciones para desarrolladores @@ -212,6 +221,30 @@ changes the app and the language of the speech-recognition-based search to 'Russian'. --> Ruso + + Polaco + + + Danés + + + Noruego + + + Sueco + + + Finlandés + + + Holandés + Visualización @@ -237,6 +270,10 @@ opens the Firfox Reality support web site in the browser window. --> Ayuda + + Enviar comentarios + Reinicio requerido @@ -478,6 +515,14 @@ of this storage even though the storage may not reside on external removable media. --> Leer almacenamiento externo + + Bloquear ventanas emergentes + + + Avanzado + %1$s Recopilación y uso de datos @@ -740,6 +785,15 @@ Restablecer los ajustes de Privacidad & Seguridad + + Limpiar todo + + + Restablecer configuración de ventanas emergentes + + + Los siguientes sitios web han solicitado abrir ventanas emergentes: + Restablecer ajustes del controlador @@ -759,13 +813,13 @@ Restablecer todas las configuraciones de idioma -   %1$s]]> + Idioma de búsqueda por voz: -   %1$s]]> + Idioma preferido para mostrar sitios web: -   %1$s]]> + Idioma para mostrar la aplicación: Idioma(s) preferido(s) @@ -852,8 +906,23 @@ and hovers over the 'Private Browsing' icon in the browser's tray menu. --> Salir de la navegación privada - - Abrir en una ventana nueva. + + Copiar enlace + + + Cortar + + + Copiar + + + Pegar + + + Seleccionar todo + + + Desmarcar @@ -985,4 +1054,75 @@ Limpiar - + + + %1$s evitó una ventana emergente de este sitio + + + ¿Te gustaría mostrarlos de todos modos? + + + No volver a preguntar en este sitio + + + Mostrar + + + Seleccionar + + + Hecho + + + Seleccionar todo + + + Cerrar pestañas + + + Cerrar todo + + + Desmarcar todo + + + 1 pestaña + + + %1$s pestañas + + + Una pestaña seleccionada + + + %1$s pestañas seleccionadas + + + 0 pestañas seleccionadas + + + Ver pestañas + + + ¡Nueva pestaña añadida! + + + diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index a095baffc..b0d675fd4 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -124,6 +124,19 @@ opens a dialog box that contains settings that an application or Web developer might want to change. --> Kehittäjävalinnat + + Kirjaudu tilille + + + Hallitse tiliä + + + Kirjaudu ulos + + + Yhdistä uudelleen + Kieli @@ -224,6 +237,22 @@ changes the app and the language of the speech-recognition-based search to 'Danish'. --> tanska + + norja + + + ruotsi + + + suomi + + + hollanti + Ohjainvalinnat @@ -241,10 +270,24 @@ a the survey web site in opened in the currently focused window --> Lähetä palautetta + + Kirjaudu sisään + + + Kirjaudu ulos + + + Yhdistä uudelleen + Uudelleenkäynnistys vaaditaan + + %1$s pitää käynnistää uudelleen, jotta muutokset tulevat voimaan. Haluatko tehdä sen nyt? + Käynnistä uudelleen nyt @@ -353,6 +396,60 @@ for the home page. --> Aloitussivu + + Tili + + + Synkronointiasetukset + + + Synkronoi nyt + + + Synkronoidaaan... + + + Kirjanmerkit + + + Historia + + + Kirjaudu tilillesi synkronoidaksesi + + + Synkronointiasetukset + + + Viimeksi synkronoitu nyt + + + Ei vielä synkronoitu + + + Synkronointi on kytketty pois päältä + + + Synkronointivirhe + + + Viimeksi synkronoitu %1$d minuuttia sitten + + + Toista DRM-suojattua sisältöä (<a href="http://somesite.com/">Lue lisää</a>) + + + Palauta välilehdet ja ikkunat uudelleenkäynnistyksen jälkeen + Seurannan suojaus @@ -373,6 +470,10 @@ notifications. --> Ilmoitukset + + Kamera Ja mikrofoni + Estä ponnahdusikkunat @@ -383,6 +484,10 @@ Pois + + Älä synkronoi + Mitä haluat etsiä verkosta? @@ -403,6 +508,9 @@ device's microphone. It explains why Voice Search is not working. --> Salli mikrofonin käyttö käyttääksesi äänihakua. + + Sulje + Hae verkosta tai kirjoita osoite @@ -490,6 +598,21 @@ Ladataan kirjanmerkkejä + + Kannettavan laitteen kirjanmerkit + + + Työpöydän kirjanmerkit + + + Kirjanmerkkivalikko + + + Kirjanmerkkipalkki + + + Muut kirjanmerkit + @@ -546,6 +669,24 @@ clearing history is displayed. --> Tyhjennä historia + + Synkronoidaan… + + + Haluatko kirjautua ulos Firefox-tililtäsi? + + + Tyhjennä historia ja kirjanmerkit tältä laitteelta + + + Kirjaudu ulos + + + Peruuta + Peruuta @@ -578,6 +719,10 @@ the more button is pressed in a history item. It opens the selected history item in a new window. --> Avaa uuteen ikkunaan + + Avaa uuteen välilehteen + Lisää kirjanmerkkeihin @@ -693,6 +838,10 @@ browser's navigation bar. The button it labels, when pressed, clears the navigation bar text. --> Tyhjennä + + Ponnahdusikkuna estetty + Näytä sivuston tiedot. @@ -714,10 +863,10 @@ Poistu yksityisestä selauksesta - Avaa uuteen ikkunaan. + Avaa uuteen ikkunaan - Avaa uuteen välilehteen. + Avaa uuteen välilehteen Kopioi linkki @@ -791,10 +940,18 @@ Internetyhteys menetettiin + + Vain %1$s ikkunaa voi olla avoina. Sulje yksi ikkuna avataksesi uuden. + OK + + Suorittaminen ei onnistu + Salli @@ -816,13 +973,24 @@ when there is one window left. When clicked, it closes the private session --> Tyhjennä + + %1$s esti ponnahdusikkunan tältä sivustolta + + + Haluatko nähdä ponnahdusikkunat silti? + + + Älä kysy uudestaan tämän sivuston kohdalla + Näytä - Älä näytä + Peruuta @@ -844,6 +1012,10 @@ the Select` button. When clicked it closes all the previously selected tabs --> When clicked it closes all the available tabs --> Sulje kaikki + + 1 välilehti + @@ -869,4 +1041,69 @@ the Select` button. When clicked it closes all the previously selected tabs --> Uusi välilehti lisätty! + + + Välilehti lähetetty! + + + Lähetä välilehti laitteeseen + + + Työpöytätila + + + Valikko + + + Lähetä välilehti laitteeseen + + + Valitse laite, jolle välilehti lähetetään. + + + Käytettävissä olevat laitteet: + + + Ei yhdistettyjä laitteita. + + + Synkronoidaan... + + + Lähetä + + + Kirjaudu sisään lähettääksesi välilehtiä + + + Firefox-tili mahdollistaa kirjanmerkkien ja historian synkronoinnin kaikkien laitteidesi välillä. + + + Kirjaudu sisään + + + Aloita selaaminen + + + + Hidas komentosarja + + + Verkkosivu hidastaa selaintasi (%1$s). Mitä haluat tehdä? + + + Pysäytä se + + + Odota diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 293f14f5b..9906966b6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -4,7 +4,7 @@ https://github.com/MozillaReality/FirefoxReality/wiki/L10n --> - ENTRER + ENTRÉE Options de développement + + Connectez-vous à votre compte + Gérer le compte + + Déconnexion + Se reconnecter @@ -158,7 +164,7 @@ 'Firefox Display Language' dialog window (accessible from the browser's Settings dialog window). Below this string appears a list of locales for the user to choose as their preferred display language for the application UI. --> - Langue d\'affichage de %1$s + Langue d’affichage de %1$s Aide + + Donnez-nous votre avis + + + Connexion + + + Déconnexion + Se reconnecter @@ -332,6 +348,12 @@ --> Activer la journalisation du débogage + + Activer l’accélération matérielle de l’interface graphique + Activer Servo @@ -506,9 +528,24 @@ Réinitialiser + + Se connecter au compte pour synchroniser + Paramètres de synchronisation + + Dernière synchronisation : à l’instant + + + Pas encore synchronisé + + + La synchronisation est désactivée + Erreur de synchronisation @@ -521,6 +558,10 @@ and is used to enable or disable playback of DRM controlled content. --> Lire du contenu contrôlé par DRM (<a href="http://somesite.com/">En savoir plus</a>) + + Restaurer les onglets et les fenêtres après le redémarrage + Protection contre le pistage @@ -560,6 +601,10 @@ of this storage even though the storage may not reside on external removable media. --> Accéder en lecture au stockage externe + + Caméra et microphone + Bloquer les fenêtres popup @@ -578,8 +623,7 @@ - Autoriser %1$s à envoyer des données techniques et d’interaction à Mozilla - + Autoriser %1$s à envoyer des données techniques et d’interaction à Mozilla @@ -591,6 +635,14 @@ Désactivé + + Synchronisation + + + Ne pas synchroniser + Que souhaitez-vous rechercher sur le Web ? @@ -601,7 +653,7 @@ - Recherche en cours... + Recherche en cours… @@ -615,9 +667,12 @@ device's microphone. It explains why Voice Search is not working. --> Vous devez autoriser l’accès à votre microphone pour permettre la commande vocale. + + Fermer + - Rechercher sur Internet ou saisir une adresse + Rechercher sur le Web ou saisir une adresse @@ -770,6 +825,10 @@ all the bookmarks. --> Afficher tous les marque-pages + + Synchroniser + Historique @@ -798,9 +857,31 @@ clearing history is displayed. --> Effacer l’historique + + Synchroniser + Synchronisation… + + Vous déconnecter de votre compte Firefox ? + + + SI vous vous déconnectez, vous ne pourrez plus envoyer ni recevoir d’onglets d’autres appareils. Vos marque-pages et votre historique cesseront également de se synchroniser. + + + Effacer l’historique et les marque-pages de cet appareil + + + Déconnexion + + + Annuler + Annuler @@ -860,6 +941,14 @@ Les sites web suivants ont demandé à ouvrir des fenêtres popup : + + Lorsque vous ouvrirez un site web avec des fenêtres popup, vous verrez une boîte de dialogue vous permettant de bloquer les fenêtres popup. + + + Une liste de sites avec leurs autorisations apparaîtra ici. + Réinitialiser les paramètres du contrôleur @@ -952,6 +1041,10 @@ browser's navigation bar. The button it labels, when pressed, clears the navigation bar text. --> Effacer + + Fenêtre popup bloquée + Afficher les informations du site @@ -972,6 +1065,12 @@ and hovers over the 'Private Browsing' icon in the browser's tray menu. --> Quitter la navigation privée + + Ouvrir dans une nouvelle fenêtre + + + Ouvrir dans un nouvel onglet + Copier le lien @@ -1035,17 +1134,15 @@ - Lorsque vous naviguez dans une fenêtre privée, %1$s n’enregistre pas:

+ Lorsque vous naviguez dans une fenêtre privée, %1$s n’enregistre pas :

  • les pages consultées
  • -
  • recherches
  • -
  • cookies
  • -
  • fichiers temporaires
  • +
  • les recherches
  • +
  • les cookies
  • +
  • les fichiers temporaires

La navigation privée ne vous offre pas l’anonymat sur Internet. Votre fournisseur d’accès à Internet ou votre employeur peuvent toujours connaître les pages que vous visitez.

-

En savoir plus Navigation privée.

- ]]>
+

En savoir plus sur la navigation privée.

]]>
@@ -1121,6 +1218,10 @@ when there is one window left. When clicked, it closes the private session --> Effacer + + %1$s a empêché ce site d’ouvrir une fenêtre popup + Voulez-vous les afficher tout de même ? @@ -1132,4 +1233,135 @@ If the button is clicked the pop-up will be shown --> Afficher - + + Annuler + + + + Sélectionner + + + Terminé + + + Tout sélectionner + + + Fermer les onglets + + + Tout fermer + + + Tout désélectionner + + + 1 onglet + + + %1$s onglets + + + 1 onglet sélectionné + + + %1$s onglets sélectionnés + + + 0 onglet sélectionné + + + Afficher les onglets + + + Nouvel onglet ajouté ! + + + Onglet envoyé ! + + + Envoyer l’onglet à un appareil + + + Redimensionnement de la fenêtre + + + Version ordinateur + + + Menu + + + Envoyer l’onglet à un appareil + + + Choisissez un appareil pour envoyer l’onglet. + + + Appareils disponibles : + + + Aucun appareil connecté. + + + Synchronisation… + + + Envoyer + + + Connectez-vous pour envoyer des onglets + + + Connectez-vous ou créez un nouveau compte Firefox pour envoyer des onglets depuis Firefox sur votre ordinateur ou votre appareil mobile vers votre casque. + + + Un compte Firefox vous permet également de synchroniser les marque-pages et l’historique entre tous vos appareils. + + + Se connecter + + + Commencer la navigation + + + + Script lent + + + Une page web ralentit votre navigateur (%1$s). Que voulez-vous faire ? + + + L’arrêter + + + Patienter + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 39184ead9..f2e567934 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -124,6 +124,19 @@ opens a dialog box that contains settings that an application or Web developer might want to change. --> Opzioni per gli sviluppatori + + Accedi all’account + + + Gestisci account + + + Disconnetti + + + Riconnetti + Lingua @@ -274,6 +287,16 @@ a the survey web site in opened in the currently focused window --> Invia feedback + + Accedi + + + Disconnetti + + + Riconnetti + Riavvio necessario @@ -325,6 +348,12 @@ --> Attiva i log di debug + + Attiva l’accelerazione hardware dell’interfaccia utente + Abilita Servo @@ -472,10 +501,67 @@ for the home page. --> Pagina iniziale + + Account + + + Impostazioni di sincronizzazione + + + Sincronizza adesso + + + Sincronizzazione in corso… + + + Scegli gli elementi di Firefox da sincronizzare tra i dispositivi + + + Segnalibri + + + Cronologia + + + Ripristina + + + Accedi all’account per sincronizzare + + + Impostazioni di sincronizzazione + + + Ultima sincronizzazione: adesso + + + Non ancora sincronizzato + + + La sincronizzazione è disattivata + + + Errore di sincronizzazione + + + Ultima sincronizzazione: %1$d minuti fa + Riproduci contenuti protetti da DRM (<a href="http://somesite.com/">Scopri di più</a>) + + Ripristina schede e finestre dopo il riavvio + Protezione antitracciamento @@ -515,6 +601,10 @@ of this storage even though the storage may not reside on external removable media. --> Lettura memoria esterna + + Fotocamera e microfono + Blocca finestre pop-up @@ -545,6 +635,14 @@ Disattivato + + Sincronizza + + + Non sincronizzare + Cosa si desidera cercare sul web? @@ -569,6 +667,9 @@ device's microphone. It explains why Voice Search is not working. --> Consentire l’accesso al microfono per utilizzare la ricerca vocale. + + Chiudi + Cerca sul Web o inserisci un indirizzo @@ -681,6 +782,21 @@ Caricamento dei segnalibri in corso… + + Segnalibri da dispositivi mobile + + + Segnalibri pc desktop + + + Menu segnalibri + + + Barra dei segnalibri + + + Altri segnalibri + @@ -709,6 +825,10 @@ all the bookmarks. --> Visualizza tutti i segnalibri + + Sincronizza + Cronologia @@ -737,6 +857,31 @@ clearing history is displayed. --> Cancella cronologia + + Sincronizza + + + Sincronizzazione in corso… + + + Desideri disconnetterti dal tuo account Firefox? + + + Quando ti disconnetti, non potrai più inviare o ricevere scheda dagli altri dispositivi. Anche la sincronizzazione dei segnalibri e della cronologia verrà interrotta. + + + Cancella la cronologia e i segnalibri da questo dispositivo + + + Disconnetti + + + Annulla + Annulla @@ -769,6 +914,10 @@ the more button is pressed in a history item. It opens the selected history item in a new window. --> Apri in una nuova finestra + + Apri in una nuova scheda + Aggiungi ai segnalibri @@ -792,6 +941,14 @@ I seguenti siti web hanno richiesto l’apertura di finestre pop-up: + + Quando si apre un sito web con finestre pop-up, verrà visualizzata una finestra di dialogo che consente di scegliere se bloccare le finestre pop-up. + + + In questo spazio verrà visualizzato l’elenco di siti con permessi. + Ripristina impostazioni controller @@ -884,6 +1041,10 @@ browser's navigation bar. The button it labels, when pressed, clears the navigation bar text. --> Cancella + + Pop-up bloccato + Visualizza informazioni sul sito @@ -905,10 +1066,10 @@ Lascia la modalità di navigazione anonima - Apri in una nuova finestra. + Apri in una nuova finestra - Apri in una nuova scheda. + Apri in una nuova scheda Copia link @@ -1079,7 +1240,7 @@ - Nascondi + Annulla @@ -1134,4 +1295,78 @@ the Select` button. When clicked it closes all the previously selected tabs --> Nuova scheda aggiunta! + + + + Scheda inviata. + + + Invia scheda a dispositivo + + + Ridimensionamento della finestra + + + Modalità desktop + + + Menu + + + Invia scheda a dispositivo + + + Scegli un dispositivo a cui inviare la scheda. + + + Dispositivi disponibili: + + + Nessun dispositivo connesso. + + + Sincronizzazione in corso... + + + Invia + + + Accedi per inviare schede + + + Accedi o crea un account Firefox per inviare schede dai tuoi dispositivi al visore VR attraverso Firefox. + + + L’account Firefox ti consente anche di sincronizzare i segnalibri e la cronologia tra tutti i tuoi dispositivi. + + + Accedi + + + Inizia a navigare + + + + Script lento + + + La pagina web “%1$s” rallenta il funzionamento del browser. Come procedere? + + + Bloccala + + + Attendi diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7348727e5..5a52eb991 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -124,6 +124,19 @@ opens a dialog box that contains settings that an application or Web developer might want to change. --> 開発者オプション + + アカウントにログイン + + + アカウントを管理 + + + ログアウト + + + 再接続 + 言語 @@ -274,6 +287,16 @@ a the survey web site in opened in the currently focused window --> フィードバックを送る + + ログイン + + + ログアウト + + + 再接続 + 再起動が必要です @@ -325,6 +348,12 @@ --> デバッグログを有効にする + + UI のハードウェアアクセラレーションを有効化 + Servo を有効化 @@ -472,10 +501,67 @@ for the home page. --> ホームページ + + アカウント + + + Sync 設定 + + + 今すぐ同期 + + + 同期中... + + + 端末上の同期して Firefox で使用するものを選んでください + + + ブックマーク + + + 履歴 + + + リセット + + + アカウントにログインして同期 + + + Sync 設定 + + + 最終同期日時: 直前 + + + まだ同期していません + + + 同期がオフです + + + Sync エラー + + + 最終同期日時: %1$d 分前 + DRM 制御のコンテンツを再生 (<a href="http://somesite.com/">詳細</a>) + + 再起動後にタブとウィンドウを復元する + トラッキング防止 @@ -515,6 +601,10 @@ of this storage even though the storage may not reside on external removable media. --> 外部ストレージの読み取り + + カメラとマイク + ポップアップウィンドウをブロックする @@ -546,6 +636,14 @@ オフ + + 同期する + + + 同期しない + 何を検索しますか? @@ -570,6 +668,9 @@ device's microphone. It explains why Voice Search is not working. --> 音声検索を使用するにはマイクへのアクセスを許可してください。 + + 閉じる + 検索語またはアドレスを入力してください @@ -682,6 +783,21 @@ ブックマークを読み込んでいます + + モバイルのブックマーク + + + デスクトップのブックマーク + + + ブックマークメニュー + + + ブックマークツールバー + + + 他のブックマーク + @@ -710,6 +826,10 @@ all the bookmarks. --> すべてのブックマークを表示 + + 同期 + 履歴 @@ -738,6 +858,31 @@ clearing history is displayed. --> 履歴を消去 + + 同期 + + + 同期中... + + + Firefox アカウントからログアウトしますか? + + + ログアウトすると、他の端末とタブを送受信できなくなります。ブックマークと履歴の同期も停止します。 + + + この端末から履歴とブックマークを消去する + + + ログアウト + + + キャンセル + キャンセル @@ -770,6 +915,10 @@ the more button is pressed in a history item. It opens the selected history item in a new window. --> 新しいウィンドウで開く + + 新しいタブで開く + ブックマークに追加 @@ -793,6 +942,14 @@ 次のウェブサイトがポップアップウィンドウの表示を要求しています: + + ポップアップウィンドウが表示されるウェブサイトを開くと、ポップアップのブロックを選択するダイアログが表示されます。 + + + 許可したサイトのリストがここに表示されます。 + コントローラーの設定をリセット @@ -885,6 +1042,10 @@ browser's navigation bar. The button it labels, when pressed, clears the navigation bar text. --> 消去します + + ポップアップをブロックしました + サイトの情報を表示 @@ -906,10 +1067,10 @@ プライベートブラウジングを終了 - 新しいウィンドウで開く + 新しいウィンドウで開く - 新しいタブで開く + 新しいタブで開く リンクをコピー @@ -1077,7 +1238,7 @@ - 表示しない + キャンセル @@ -1132,4 +1293,78 @@ the Select` button. When clicked it closes all the previously selected tabs --> 新しいタブが追加されました! + + + + タブを送信しました + + + タブを端末へ送信 + + + ウィンドウサイズを変更 + + + デスクトップモード + + + メニュー + + + タブを端末へ送信 + + + タブの送信先の端末を選んでください。 + + + 利用可能な端末: + + + 接続された端末がありません。 + + + 同期中... + + + 送信 + + + ログインしてタブを送信 + + + Firefox アカウントにログインまたはアカウント登録をして、タブをデスクトップまたはモバイル端末の Firefox からヘッドセットへ送信できます。 + + + Firefox アカウントを利用して、ブックマークや履歴をご使用のすべての端末と同期することもできます。 + + + ログイン + + + ブラウジングを始める + + + + 動作の遅いスクリプト + + + ウェブページ (%1$s) がブラウザーの動作を遅くしています。どうしますか? + + + 停止 + + + 待機 diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 3b1957f98..3e864e3eb 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -349,6 +349,12 @@ --> Aktiver feilsøking + + Aktiver UI-maskinvareakselerasjon + Slå på Servo @@ -553,6 +559,10 @@ and is used to enable or disable playback of DRM controlled content. --> Spill av DRM-kontrollert innhold (<a href="http://somesite.com/">les mer</a>) + + Gjenopprett faner og vinduer etter omstart + Sporingsbeskyttelse @@ -592,6 +602,10 @@ of this storage even though the storage may not reside on external removable media. --> Les ekstern lagring + + Kamera og mikrofon + Blokker sprettoppvinduer @@ -655,6 +669,9 @@ device's microphone. It explains why Voice Search is not working. --> Tillat mikrofontilgang for å bruke stemmesøk. + + Lukk + Søk på nettet eller skriv inn adresse @@ -850,6 +867,24 @@ Synkroniserer… + + Logge ut av Firefox-kontoen din? + + + Når du logger deg ut, vil du ikke kunne sende eller motta faner fra andre enheter. Bokmerkene og historikken din slutter også å synkronisere. + + + Fjern historikk og bokmerker fra denne enheten + + + Logg ut + + + Avbryt + Avbryt @@ -909,6 +944,14 @@ Følgende nettsteder har bedt om å åpne sprettoppvinduer: + + Når du åpner et nettsted med sprettoppvinduer, vil du se en dialog som lar deg velge å blokkere sprettoppvinduer. + + + En liste over nettsteder med tillatelser vises her. + Tilbakestill kontrollerinnstillinger diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 7247d7243..3d8e94ee0 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -349,6 +349,12 @@ --> Debuglogging inschakelen + + UI-hardwareversnelling inschakelen + Servo inschakelen @@ -462,7 +468,7 @@ - Ongeldig + Leeg DRM-gecontroleerde inhoud afspelen (<a href="http://somesite.com/">Meer info</a>) + + Tabbladen en vensters herstellen na herstarten + Bescherming tegen volgen @@ -594,6 +604,10 @@ of this storage even though the storage may not reside on external removable media. --> Externe opslag lezen + + Camera en microfoon + Pop-upvensters blokkeren @@ -658,6 +672,9 @@ device's microphone. It explains why Voice Search is not working. --> Sta microfoontoegang toe om gesproken zoekopdrachten te gebruiken. + + Sluiten + Zoek op het web of voer adres in @@ -853,6 +870,24 @@ Synchroniseren… + + Afmelden bij uw Firefox-account? + + + Wanneer u zicht afmeldt, kunt u geen tabbladen van andere apparaten ontvangen of verzenden. Uw bladwijzers en geschiedenis worden ook niet meer gesynchroniseerd. + + + Geschiedenis en bladwijzers van dit apparaat wissen + + + Afmelden + + + Annuleren + Annuleren @@ -912,6 +947,14 @@ De volgende websites hebben het openen van pop-upvensters aangevraagd: + + Wanneer u een website met pop-upvensters opent, ziet u een dialoogvenster waarin u kunt kiezen om pop-upvensters te blokkeren. + + + Een lijst van websites met machtigingen zal hier verschijnen. + Controllerinstellingen herinitialiseren diff --git a/app/src/main/res/values-nn-rNO/strings.xml b/app/src/main/res/values-nn-rNO/strings.xml index e0bf1aff3..199e19129 100644 --- a/app/src/main/res/values-nn-rNO/strings.xml +++ b/app/src/main/res/values-nn-rNO/strings.xml @@ -349,6 +349,12 @@ --> Aktiver feilsøking + + Aktiver UI-maskinvareakselerasjon + Slå på Servo @@ -553,6 +559,10 @@ and is used to enable or disable playback of DRM controlled content. --> Spel av DRM-kontrollert innhald (<a href="http://somesite.com/">les meir</a>) + + Rett oppatt faner og vindauge etter omstart + Sporingsvern @@ -592,6 +602,10 @@ of this storage even though the storage may not reside on external removable media. --> Les ekstern lagring + + Kamera og mikrofon + Blokker sprettoppvindauge @@ -655,6 +669,9 @@ device's microphone. It explains why Voice Search is not working. --> Tillat mikrofontilgang for å bruke stemmesøk. + + Lat att + Søk på nettet eller skriv inn adresse @@ -849,6 +866,24 @@ Synkroniserer… + + Logge ut av Firefox-kontoen din? + + + Når du loggar deg ut, vil du ikkje kunne sende eller motta faner frå andre einingar. Bokmerka og historikken din sluttar også å synkronisere. + + + Fjern historikk og bokmerke frå denne eininga + + + Logg ut + + + Avbryt + Avbryt @@ -908,6 +943,14 @@ Følgjande nettstadar har bedt om å opne sprettoppvindauge: + + Når du opnar ein nettstad med sprettoppvindauge, vil du sjå ein dialog som lar deg velje å blokkere sprettoppvindauge. + + + Ei liste over nettstadar med løyve vert viste her. + Tilbakestill kontrollerinnstillingar @@ -1311,4 +1354,20 @@ the Select` button. When clicked it closes all the previously selected tabs --> Start nettlesing + + + + Tregt skript + + + Ei nettside gjer at nettlesaren går tregare (%1$s). Kva vil du gjere? + + + Stopp den + + + Vent diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index b1242b0b1..31e950583 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -349,6 +349,12 @@ --> Rejestrowanie debugowania + + Sprzętowe przyspieszanie interfejsu + Silnik Servo @@ -521,7 +527,7 @@ Historia - Przywróć domyślne + Przywróć Zaloguj się na konto, aby synchronizować @@ -553,6 +559,10 @@ and is used to enable or disable playback of DRM controlled content. --> Odtwarzanie treści chronionych przez DRM (<a href="http://somesite.com/">więcej informacji</a>) + + Przywracanie kart i okien po ponownym uruchomieniu + Ochrona przed śledzeniem @@ -593,6 +603,10 @@ of this storage even though the storage may not reside on external removable media. --> Odczyt pamięci zewnętrznej + + Kamera i mikrofon + Blokuj wyskakujące okna @@ -656,6 +670,9 @@ device's microphone. It explains why Voice Search is not working. --> Wyszukiwanie głosowe wymaga zezwolenia na dostęp do mikrofonu. + + Zamknij + Wpisz adres lub szukaj w Internecie @@ -850,6 +867,24 @@ Synchronizowanie… + + Czy wylogować się z konta Firefoksa? + + + Po wylogowaniu nie będzie można wysyłać ani odbierać kart z innych urządzeń. Zakładki i historia także przestaną być synchronizowane. + + + Usuń historię i zakładki na tym urządzeniu + + + Wyloguj się + + + Anuluj + Anuluj @@ -909,6 +944,14 @@ Te witryny poprosiły o zezwolenie na otwieranie wyskakujących okien: + + Po otwarciu witryny z wyskakującym oknem zostanie wyświetlone okno dialogowe umożliwiające wybranie opcji ich blokowania. + + + Tutaj będą wyświetlane witryny z zezwoleniem na otwieranie wyskakujących okien. + Przywróć ustawienia kontrolera diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 464b34a11..a898e0d70 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -125,6 +125,19 @@ opens a dialog box that contains settings that an application or Web developer might want to change. --> Настройки разработчика + + Войти в аккаунт + + + Управление аккаунтом + + + Выйти + + + Переподключиться + Язык @@ -275,6 +288,16 @@ a the survey web site in opened in the currently focused window --> Отправьте свой отзыв + + Войти + + + Выйти + + + Переподключиться + Требуется перезапуск @@ -326,6 +349,12 @@ --> Включить отладочное логирование + + Включить аппаратное ускорение интерфейса + Включить Servo @@ -473,10 +502,67 @@ for the home page. --> Домашняя страница + + Аккаунт + + + Параметры синхронизации + + + Синхронизировать + + + Синхронизация... + + + Выберите, что нужно синхронизировать на ваших устройствах при помощи Firefox + + + Закладки + + + Историю + + + Сбросить + + + Войдите в аккаунт, чтобы начать синхронизацию + + + Параметры синхронизации + + + Синхронизировано только что + + + Ещё не синхронизировано + + + Синхронизация отключена + + + Ошибка синхронизации + + + Последняя синхронизация: %1$d минут назад + Воспроизводить контролируемое DRM содержимое (<a href="http://somesite.com/">Узнать больше</a>) + + Восстанавливать вкладки и окна после перезапуска + Защита от отслеживания @@ -516,6 +602,10 @@ of this storage even though the storage may not reside on external removable media. --> Чтение внешнего хранилища + + Камера и микрофон + Блокировка всплывающих окон @@ -547,6 +637,14 @@ Отключено + + Синхронизировать + + + Не синхронизировать + Что бы вы хотели найти в Интернете? @@ -571,6 +669,9 @@ device's microphone. It explains why Voice Search is not working. --> Пожалуйста, разрешите доступ к микрофону, чтобы использовать голосовой поиск. + + Закрыть + Найдите в Интернете или введите адрес @@ -683,6 +784,21 @@ Загружаем закладки + + Мобильные закладки + + + Закладки на компьютере + + + Меню закладок + + + Панель закладок + + + Другие закладки + @@ -711,6 +827,10 @@ all the bookmarks. --> Все закладки + + Синхронизировать + История @@ -739,6 +859,31 @@ clearing history is displayed. --> Удалить историю + + Синхронизировать + + + Синхронизация… + + + Выйти из вашего Аккаунта Firefox? + + + После выхода из аккаунта, вы больше не сможете отправлять или получать вкладки с устройств. Ваши закладки и история также прекратят синхронизироваться. + + + Удалить историю и закладки с этого устройства + + + Выйти + + + Отмена + Отмена @@ -771,6 +916,10 @@ the more button is pressed in a history item. It opens the selected history item in a new window. --> Открыть в новом окне + + Открыть в новой вкладке + Добавить в закладки @@ -794,6 +943,14 @@ Следующие сайты хотят открывать всплывающие окна: + + Открыв веб-сайт со всплывающими окнами, вы увидите сообщение с предложением заблокировать всплывающие окна. + + + Список разрешённых сайтов появится здесь. + Сбросить настройки контроллера @@ -887,6 +1044,10 @@ browser's navigation bar. The button it labels, when pressed, clears the navigation bar text. --> Очистить + + Всплывающее окно заблокировано + Показать информацию о сайте. @@ -908,10 +1069,10 @@ Выйти из Приватного просмотра - Открыть в новом окне. + Открыть в новом окне - Открыть в новой вкладке. + Открыть в новой вкладке Копировать ссылку @@ -1078,7 +1239,7 @@ - Не показывать + Отмена @@ -1133,4 +1294,77 @@ the Select` button. When clicked it closes all the previously selected tabs --> Добавлена новая вкладка! + + + Вкладка отправлена! + + + Отправить вкладку на устройство + + + Изменить размеры окна + + + Версия для ПК + + + Меню + + + Отправить вкладку на устройство + + + Выберите устройство, на которое нужно отправить вкладку. + + + Доступные устройства: + + + Ни одного устройства не подключено. + + + Синхронизация... + + + Отправить + + + Войдите, чтобы отправлять вкладки + + + Войдите или создайте новый Аккаунт Firefox, чтобы отправлять вкладки с Firefox на ПК или мобильного устройства на вашу гарнитуру. + + + Аккаунт Firefox также позволит вам синхронизировать закладки и историю на всех ваших устройствах. + + + Войти + + + Начать веб-сёрфинг + + + + Тяжёлый скрипт + + + Веб-страница замедляет ваш браузер (%1$s). Что бы вы хотели сделать? + + + Остановить её + + + Подождать diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 6fcf3c3c1..cd90fafb9 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -348,6 +348,12 @@ --> Aktivera felsökning + + Aktivera UI-maskinvaruacceleration + Aktivera Servo @@ -552,6 +558,10 @@ and is used to enable or disable playback of DRM controlled content. --> Spela DRM-kontrollerat innehåll (<a href="http://somesite.com/">Läs mer</a>) + + Återställ flikar och fönster efter omstart + Spårningsskydd @@ -591,6 +601,10 @@ of this storage even though the storage may not reside on external removable media. --> Läsa extern lagring + + Kamera och mikrofon + Blockera popup-fönster @@ -654,6 +668,9 @@ device's microphone. It explains why Voice Search is not working. --> Tillåt mikrofonåtkomst för att använda röstsökningen. + + Stäng + Sök på webben eller ange adress @@ -848,6 +865,24 @@ Synkroniserar… + + Logga ut från ditt Firefox-konto? + + + När du loggar ut kommer du inte att kunna skicka eller ta emot flikar från andra enheter. Dina bokmärken och historik slutar också synkronisera. + + + Rensa historik och bokmärken från den här enheten + + + Logga ut + + + Avbryt + Avbryt @@ -907,6 +942,14 @@ Följande webbplatser har begärt att få öppna popup-fönster: + + När du öppnar en webbplats med popup-fönster ser du en dialogruta som låter dig välja att blockera popup-fönster. + + + Här visas en lista över webbplatser med behörigheter. + Återställ kontrollinställningar diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9bf0093d5..3ed3c3ab0 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -348,6 +348,12 @@ --> 启用调试日志 + + 启用 UI 硬件加速 + 启用 Servo @@ -552,6 +558,10 @@ and is used to enable or disable playback of DRM controlled content. --> 播放 DRM 控制内容(<a href="http://somesite.com/">详细了解</a>) + + 重启浏览器后恢复标签页和窗口 + 跟踪保护 @@ -591,6 +601,10 @@ of this storage even though the storage may not reside on external removable media. --> 读取外部存储 + + 相机与麦克风 + 阻止弹出窗口 @@ -653,6 +667,9 @@ device's microphone. It explains why Voice Search is not working. --> 要使用语音搜索,请许可麦克风使用权限。 + + 关闭 + 搜索网络或输入网址 @@ -775,7 +792,7 @@ 菜单 - 工具栏 + 书签工具栏 其他 @@ -847,6 +864,24 @@ 正在同步… + + 要退出 Firefox 账户吗? + + + 退出后,就无法发送标签页或收到来自其他设备的标签页,也会停止同步书签与历史记录。 + + + 清除此设备上的历史记录与书签 + + + 退出 + + + 取消 + 取消 @@ -906,6 +941,14 @@ 下列网站请求打开弹出窗口: + + 打开包含弹出窗口的网站时,会看到是否要阻止弹出窗口的对话框。 + + + 获得权限的网站将以列表形式显示于此处。 + 重置控制器设置 @@ -1305,4 +1348,20 @@ the Select` button. When clicked it closes all the previously selected tabs --> 开始浏览 + + + + 脚本运行缓慢 + + + 某个网页(%1$s)让您的浏览器变慢了。您想如何处理? + + + 停止 + + + 等待 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 4c35fbd76..45656334e 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -348,6 +348,12 @@ --> 開啟除錯紀錄 + + 開啟 UI 硬體加速 + 啟用 Servo @@ -507,11 +513,11 @@ - 同步中··· + 同步中… - 選擇要在裝置上的 Firefox 同步哪些資料。 + 選擇要同步哪些資料到您其他使用 Firefox 的裝置 書籤 @@ -552,6 +558,10 @@ and is used to enable or disable playback of DRM controlled content. --> 播放 DRM 控制內容(<a href="http://somesite.com/">了解更多</a>) + + 重新啟動後,還原分頁與視窗 + 追蹤保護 @@ -591,6 +601,10 @@ of this storage even though the storage may not reside on external removable media. --> 讀取外部儲存空間 + + 攝影機與麥克風 + 封鎖彈出視窗 @@ -653,6 +667,9 @@ device's microphone. It explains why Voice Search is not working. --> 要使用語音搜尋,請允許使用麥克風。 + + 關閉 + 請搜尋網路或輸入網址 @@ -769,7 +786,7 @@ 行動版書籤 - 桌面版書籤 + 來自電腦的書籤 選單 @@ -847,6 +864,24 @@ 同步中… + + 要登出 Firefox 帳號嗎? + + + 登出後,就無法傳送分頁或收到來自其他裝置的分頁,也會停止同步書籤與上網紀錄。 + + + 清除此裝置上的瀏覽紀錄與書籤 + + + 登出 + + + 取消 + 取消 @@ -906,6 +941,14 @@ 下列網站要求開啟彈出視窗: + + 開啟包含彈出視窗的網站時,會看到選擇是否要封鎖彈出視窗的對話框。 + + + 將於此處顯示獲得權限的網站清單。 + 重設控制器設定 @@ -1306,4 +1349,20 @@ the Select` button. When clicked it closes all the previously selected tabs --> 開始上網 + + + + 拖慢瀏覽器運作的指令碼 + + + 有張網頁讓您的瀏覽器變慢了(%1$s),您想要怎麼處理? + + + 停下來 + + + 稍候 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 97edc7fb0..4923d8dec 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -7,6 +7,7 @@ + @@ -93,5 +94,4 @@ - diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index b8e8ffc68..3cb638626 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -3,7 +3,6 @@ 800px - 800px 4.0 0.35 -4.2 @@ -43,9 +42,9 @@ 36dp 18dp 8dp - 186dp - 198dp - 50dp + 166dp + 178dp + 30dp 36dp 8dp 28dp @@ -61,29 +60,18 @@ 0dp 4dp 37dp - - 1.6 - 320dp - 260dp - 20dp - 25dp - 45dp - - 50dp - 116dp 1.6 -2.0 770dp 490dp - 320dp - 100dp 125dp - 100dp - 130dp + 100dp + 130dp + 54dp 88dp @@ -98,18 +86,12 @@ 324dp - 100dp + 105dp 284dp 85dp 15dp 2.5 - - 1.6 - -1.8 - 420dp - 280dp - 132dp 40dp @@ -119,7 +101,6 @@ 650dp 380dp 6dp - 10dp 87dp 15dp 2dp @@ -132,8 +113,6 @@ 36dp - 160dp - 4dp 56dp 34dp 74dp @@ -155,54 +134,32 @@ 385dp - 120dp + 140dp + 30dp + 30dp 40dp - - 460dp - 280dp - 20dp - 25dp - 30dp - 2 - 305dp 65dp + 400dp 800dp 450dp 300dp 520dp 400dp - - 640dp - 350dp - 1.2 - -1.8 - - 156dp - 175dp - 35dp - 20dp - 14dip - 20dp - 20dp - 30dp - 28sp 24sp 22sp 18sp 16sp 14sp - 12sp 11sp - 8sp 20sp 14sp @@ -218,13 +175,9 @@ 10dp 8dp - 2dp - 0dp - 38dp 64dp - 18sp 20sp 16sp 16sp @@ -234,7 +187,6 @@ 65dp - 22dp 20dp 120dp @@ -244,6 +196,10 @@ 20dp 0.1 + + 56dp + 300dp + 320dp 60dp @@ -253,10 +209,6 @@ 2dp 1dp - - 320dp - 260dp - 0.02 1000 @@ -265,43 +217,34 @@ 24sp 3.0 - - 145dp - 65dp + + 585dp + 360dp 585dp 490dp - - 585dp - 360dp - 585dp 420dp - - 450dp - 340dp - 2 - 1.2 - - - 585dp + + 1.2 - 120dp 10dp - 20dp 300dp 60dp 30dp - - 450dp - 340dp + + 450dp + 375dp + 20dp + 42dp + 64dp \ No newline at end of file diff --git a/app/src/main/res/values/integer.xml b/app/src/main/res/values/integer.xml deleted file mode 100644 index 5a4a9147f..000000000 --- a/app/src/main/res/values/integer.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - 75 - \ No newline at end of file diff --git a/app/src/main/res/values/non_L10n.xml b/app/src/main/res/values/non_L10n.xml index d486d0743..ed9de5733 100644 --- a/app/src/main/res/values/non_L10n.xml +++ b/app/src/main/res/values/non_L10n.xml @@ -11,7 +11,7 @@ settings_remote_debugging settings_console_logs settings_environment_override - settings_environment_multiprocess_e10s + settings_environment_multiprocess_e10s_v2 settings_performance_monitor settings_environment_servo settings_key_drm_playback @@ -49,6 +49,9 @@ settings_key_bookmarks_sync settings_key_history_sync settings_key_whats_new_displayed + settings_key_ui_hardware_acceleration_v2 + settings_key_fxa_last_sync + settings_key_restore_tabs https://github.com/MozillaReality/FirefoxReality/wiki/Environments https://www.mozilla.org/privacy/firefox/ https://mixedreality.mozilla.org/fxr/report?src=browser-fxr&label=browser-firefox-reality&url=%1$s @@ -62,7 +65,7 @@ up_id down_id https://support.mozilla.org/kb/crash-reporting-firefox-reality - org.mozilla.vrbrowser.CRASH_RECEIVER_PERMISSION + CRASH_RECEIVER_PERMISSION x FirefoxReality userAgentOverride.json @@ -74,24 +77,6 @@ ABC %&= - English (US) - 繁體中文 - 简体中文 - 日本語 - Français - Deutsch - Español (España) - Español - Русский - 한국어 - Italiano - Polski - Dansk - Norsk - Svensk - Suomea - Nederlands - 选定 空格 确认 diff --git a/app/src/main/res/values/options_values.xml b/app/src/main/res/values/options_values.xml index dc126c965..0ab3c7aff 100644 --- a/app/src/main/res/values/options_values.xml +++ b/app/src/main/res/values/options_values.xml @@ -84,86 +84,6 @@ 3 - - - @string/language_en_US - @string/language_cmn_Hant_TW - @string/language_cmn_Hans_CN - @string/language_ja_JP - @string/language_fr_FR - @string/language_de_DE - @string/language_es_ES - @string/language_ru - @string/language_ko_KR - @string/language_it_IT - @string/language_da_DK - @string/language_pl_PL - @string/language_nb_NO - @string/language_sv_SE - @string/language_fi_FI - @string/language_nl_NL - - - - en-US - zh-Hant-TW - zh-Hans-CN - ja-JP - fr-FR - de-DE - es-ES - ru-RU - ko-KR - it-IT - da-DK - pl-PL - nb-NO - sv-SE - fi-FI - nl-NL - - - - - @string/language_en_US - @string/language_cmn_Hant_TW - @string/language_cmn_Hans_CN - @string/language_ja_JP - @string/language_fr_FR - @string/language_de_DE - @string/language_es_ES - @string/language_es - @string/language_ru - @string/language_ko_KR - @string/language_it_IT - @string/language_da_DK - @string/language_pl_PL - @string/language_nb_NO - @string/language_sv_SE - @string/language_fi_FI - @string/language_nl_NL - - - - en-US - zh-Hant-TW - zh-Hans-CN - ja-JP - fr-FR - de-DE - es-ES - es - ru-RU - ko-KR - it-IT - da-DK - pl-PL - nb-NO - sv-SE - fi-FI - nl-NL - - @string/history_clear_range_today diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac7526bc5..69e172647 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -344,6 +344,12 @@ --> Enable Debug Logging + + Enable UI Hardware Acceleration + Enable Servo @@ -548,6 +554,10 @@ and is used to enable or disable playback of DRM controlled content. --> Play DRM-Controlled Content (<a href="http://somesite.com/">Learn More</a>) + + Restore tabs and windows after restart + Tracking Protection @@ -587,6 +597,10 @@ of this storage even though the storage may not reside on external removable media. --> Read External Storage + + Camera And Microphone + Block Pop-Up Windows @@ -651,6 +665,9 @@ device's microphone. It explains why Voice Search is not working. --> Please allow microphone access to use the Voice Search. + + Close + Search the Web or enter address @@ -671,7 +688,7 @@ - Please help Mozilla improve %1$s by sending your crash data. + Please help Mozilla improve %1$s by sending your crash data. <a href="more">Learn More</a> @@ -845,6 +862,24 @@ Syncing… + + Sign out of your Firefox Account? + + + When you sign out, you won\'t be able to send or receive tabs from other devices. Your bookmarks and history will also stop syncing. + + + Clear history and bookmarks from this device + + + Sign Out + + + Cancel + Cancel @@ -904,6 +939,14 @@ The following websites have requested to open pop-up windows: + + When you open a website with pop-up windows, you will see a dialog that lets you choose to block pop-up windows. + + + A list of sites with permissions will appear here. + Reset Controller Settings diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 19c8f30e2..1764a5a30 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -16,10 +16,8 @@ - - - - - - + + - - - - - - - -