diff --git a/android-vault b/android-vault index ddd561a8ef..15225ede6c 160000 --- a/android-vault +++ b/android-vault @@ -1 +1 @@ -Subproject commit ddd561a8ef8289f2ed248925679827b2da5ab016 +Subproject commit 15225ede6c44da8265e5cdaea34d49dbc47cb8f5 diff --git a/apps/flutter_parent/android/app/build.gradle b/apps/flutter_parent/android/app/build.gradle index 823d43f00f..5fd86f30e5 100644 --- a/apps/flutter_parent/android/app/build.gradle +++ b/apps/flutter_parent/android/app/build.gradle @@ -104,7 +104,7 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' implementation "com.squareup.okhttp3:okhttp:4.9.1" implementation 'org.jsoup:jsoup:1.11.3' implementation 'com.google.gms:google-services:4.3.14' diff --git a/apps/flutter_parent/assets/html/html_wrapper.html b/apps/flutter_parent/assets/html/html_wrapper.html index 76218ec133..990e95789b 100644 --- a/apps/flutter_parent/assets/html/html_wrapper.html +++ b/apps/flutter_parent/assets/html/html_wrapper.html @@ -26,6 +26,8 @@ height: auto; margin: 0; padding: 0; + background-color: {BACKGROUND}; + color: {COLOR}; } img { @@ -65,6 +67,7 @@ a { word-wrap: break-word; + color: {LINK_COLOR} } .lti_button { @@ -81,6 +84,10 @@ font-size: 13px; margin: auto; } + + a:visited { + color: {VISITED_LINK_COLOR} + }
diff --git a/apps/flutter_parent/lib/l10n/res/intl_id.arb b/apps/flutter_parent/lib/l10n/res/intl_id.arb index 8d63ed0338..5975fdcc49 100644 --- a/apps/flutter_parent/lib/l10n/res/intl_id.arb +++ b/apps/flutter_parent/lib/l10n/res/intl_id.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-10-28T11:03:07.232972", + "@@last_modified": "2023-08-25T11:04:20.901151", "alertsLabel": "Peringatan", "@alertsLabel": { "description": "The label for the Alerts tab", @@ -182,6 +182,19 @@ "points": {} } }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} acara}other{{date}, {eventCount} acara}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, "No Events Today!": "Tidak Ada Acara Hari Ini!", "@No Events Today!": { "description": "Title displayed when there are no calendar events for the current day", @@ -2666,5 +2679,75 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "Acceptable Use Policy": "Kebijakan Penggunaan yang Dapat Diterima", + "@Acceptable Use Policy": { + "description": "title for the acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submit": "Serahkan", + "@Submit": { + "description": "submit button title for acceptable use policy screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Either you're a new user or the Acceptable Use Policy has changed since you last agreed to it. Please agree to the Acceptable Use Policy before you continue.": "Anda pengguna baru atau Kebijakan Penggunaan yang Dapat Diterima telah berubah sejak Anda terakhir kali menyetujuinya. Silakan setujui Kebijakan Penggunaan yang Dapat Diterima sebelum Anda melanjutkan.", + "@Either you're a new user or the Acceptable Use Policy has changed since you last agreed to it. Please agree to the Acceptable Use Policy before you continue.": { + "description": "acceptable use policy screen description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "I agree to the Acceptable Use Policy.": "Saya menyetujui Kebijakan Penggunaan yang Dapat Diterima.", + "@I agree to the Acceptable Use Policy.": { + "description": "acceptable use policy switch title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "About": "Tentang", + "@About": { + "description": "Title for about menu item in settings", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "App": "App", + "@App": { + "description": "Title for App field on about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Login ID": "ID Login", + "@Login ID": { + "description": "Title for Login ID field on about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Email": "Email", + "@Email": { + "description": "Title for Email field on about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Version": "Versi", + "@Version": { + "description": "Title for Version field on about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Instructure logo": "Logo Instructure", + "@Instructure logo": { + "description": "Semantics label for the Instructure logo on the about page", + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/apps/flutter_parent/lib/main.dart b/apps/flutter_parent/lib/main.dart index deae3fbbe4..1ee693d05a 100644 --- a/apps/flutter_parent/lib/main.dart +++ b/apps/flutter_parent/lib/main.dart @@ -33,7 +33,6 @@ import 'package:flutter_parent/utils/notification_util.dart'; import 'package:flutter_parent/utils/old_app_migration.dart'; import 'package:flutter_parent/utils/remote_config_utils.dart'; import 'package:flutter_parent/utils/service_locator.dart'; -import 'package:webview_flutter/webview_flutter.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; void main() async { diff --git a/apps/flutter_parent/lib/models/course_grade.dart b/apps/flutter_parent/lib/models/course_grade.dart index dfe9b14b76..681be6364f 100644 --- a/apps/flutter_parent/lib/models/course_grade.dart +++ b/apps/flutter_parent/lib/models/course_grade.dart @@ -69,7 +69,7 @@ class CourseGrade { /// If the course contains no valid current grade or score, this flag will be true. This is usually represented in the /// UI with "N/A". bool noCurrentGrade() => - _getCurrentScore() == null && (currentGrade() == null || currentGrade()!.contains('N/A') || currentGrade()!.isEmpty); + currentScore() == null && (currentGrade() == null || currentGrade()!.contains('N/A') || currentGrade()!.isEmpty); bool _hasActiveGradingPeriod() => !_forceAllPeriods && diff --git a/apps/flutter_parent/lib/screens/help/terms_of_use_screen.dart b/apps/flutter_parent/lib/screens/help/terms_of_use_screen.dart index 531152fe15..68756af170 100644 --- a/apps/flutter_parent/lib/screens/help/terms_of_use_screen.dart +++ b/apps/flutter_parent/lib/screens/help/terms_of_use_screen.dart @@ -75,10 +75,12 @@ class _TermsOfUseScreenState extends State { // Content return WebView( - darkMode: ParentTheme.of(context)?.isWebViewDarkMode, onWebViewCreated: (controller) { - controller.loadHtml(snapshot.data!.content!, horizontalPadding: 16); - }, + controller.loadHtml(snapshot.data!.content!, + horizontalPadding: 16, + darkMode: + ParentTheme.of(context)?.isWebViewDarkMode ?? false); + }, navigationDelegate: _handleNavigation ); }, diff --git a/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart b/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart index 1b8d04272c..dd8f5e7c08 100644 --- a/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart +++ b/apps/flutter_parent/lib/screens/web_login/web_login_screen.dart @@ -134,7 +134,6 @@ class _WebLoginScreenState extends State { navigationDelegate: (request) => _navigate(context, request, verifyResult), javascriptMode: JavascriptMode.unrestricted, - darkMode: ParentTheme.of(context)?.isWebViewDarkMode, userAgent: ApiPrefs.getUserAgent(), onPageFinished: (url) => _pageFinished(url, verifyResult), onPageStarted: (url) => _pageStarted(url), @@ -243,16 +242,15 @@ class _WebLoginScreenState extends State { /// Load the authenticated url with any necessary cookies void _loadAuthUrl() async { _showLoadingState(); - CookieManager().clearCookies(); + final cookieManager = CookieManager(); + cookieManager.clearCookies(); if (widget.loginFlow == LoginFlow.siteAdmin) { - await _controller?.setAcceptThirdPartyCookies(true); if (_domain.contains('.instructure.com')) { - String cookie = 'canvas_sa_delegated=1;domain=.instructure.com;path=/;'; - await _controller?.setCookie(_domain, cookie); - await _controller?.setCookie('.instructure.com', cookie); + cookieManager.setCookie(WebViewCookie(name: 'canvas_sa_delegated', value: '1', domain: _domain)); + cookieManager.setCookie(WebViewCookie(name: 'canvas_sa_delegated', value: '1', domain: '.instructure.com')); } else { - await _controller?.setCookie(_domain, 'canvas_sa_delegated=1'); + cookieManager.setCookie(WebViewCookie(name: 'canvas_sa_delegated', value: '1', domain: _domain)); } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/web_view/canvas_web_view.dart b/apps/flutter_parent/lib/utils/common_widgets/web_view/canvas_web_view.dart index 1a1a21ee17..4052a2549f 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/web_view/canvas_web_view.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/web_view/canvas_web_view.dart @@ -230,14 +230,15 @@ class _ResizingWebViewState extends State<_ResizingWebView> with WidgetsBindingO if (_content != widgetContent) { _height = widget.initialHeight; _content = widgetContent!; - _controller?.loadHtml(_content, horizontalPadding: widget.horizontalPadding); + _controller?.loadHtml(_content, + horizontalPadding: widget.horizontalPadding, + darkMode: ParentTheme.of(context)?.isWebViewDarkMode ?? false); } Widget child = WebView( javascriptMode: JavascriptMode.unrestricted, onPageFinished: _handlePageLoaded, onWebViewCreated: _handleWebViewCreated, - darkMode: ParentTheme.of(context)?.isWebViewDarkMode == true, navigationDelegate: _handleNavigation, gestureRecognizers: _webViewGestures(), javascriptChannels: _webViewChannels(), @@ -260,7 +261,10 @@ class _ResizingWebViewState extends State<_ResizingWebView> with WidgetsBindingO } void _handleWebViewCreated(WebViewController webViewController) async { - webViewController.loadHtml(_content, baseUrl: ApiPrefs.getDomain(), horizontalPadding: widget.horizontalPadding); + webViewController.loadHtml(_content, + baseUrl: ApiPrefs.getDomain(), + horizontalPadding: widget.horizontalPadding, + darkMode: ParentTheme.of(context)?.isWebViewDarkMode ?? false); _controller = webViewController; } diff --git a/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart b/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart index 7bcbb880d5..cc186be9dc 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart @@ -48,7 +48,6 @@ class _SimpleWebViewScreenState extends State { body: WebView( javascriptMode: JavascriptMode.unrestricted, userAgent: ApiPrefs.getUserAgent(), - darkMode: ParentTheme.of(context)?.isWebViewDarkMode, gestureRecognizers: Set()..add(Factory(() => WebViewGestureRecognizer())), navigationDelegate: _handleNavigation, onWebViewCreated: (controller) { diff --git a/apps/flutter_parent/lib/utils/web_view_utils.dart b/apps/flutter_parent/lib/utils/web_view_utils.dart index cd46d97c27..ff55e34412 100644 --- a/apps/flutter_parent/lib/utils/web_view_utils.dart +++ b/apps/flutter_parent/lib/utils/web_view_utils.dart @@ -30,15 +30,20 @@ extension WebViewUtils on WebViewController { String? html, { String? baseUrl, Map? headers, - double horizontalPadding = 0}) + double horizontalPadding = 0, + bool darkMode = false}) async { String fileText = await rootBundle.loadString('assets/html/html_wrapper.html'); html = _applyWorkAroundForDoubleSlashesAsUrlSource(html); html = _addProtocolToLinks(html); html = _checkForMathTags(html); html = fileText.replaceAll('{CANVAS_CONTENT}', html); + html = html.replaceAll('{BACKGROUND}', darkMode ? '#000000' : '#ffffff'); + html = html.replaceAll('{COLOR}', darkMode ? '#ffffff' : '#000000'); + html = html.replaceAll('{LINK_COLOR}', darkMode ? '#1283C4' : '#0374B5'); + html = html.replaceAll('{VISITED_LINK_COLOR}', darkMode ? '#C74BAF' : '#BF32A4'); html = html.replaceAll('{PADDING}', horizontalPadding.toString()); - this.loadData(baseUrl, html, 'text/html', 'utf-8'); + this.loadHtmlString(html, baseUrl: baseUrl); } /** diff --git a/apps/flutter_parent/plugins/webview_flutter/LICENSE b/apps/flutter_parent/plugins/webview_flutter/LICENSE deleted file mode 100644 index ad33cf3c3e..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2018 The Chromium Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/apps/flutter_parent/plugins/webview_flutter/android/build.gradle b/apps/flutter_parent/plugins/webview_flutter/android/build.gradle deleted file mode 100644 index 9e807667a5..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/build.gradle +++ /dev/null @@ -1,41 +0,0 @@ -group 'io.flutter.plugins.webviewflutter' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath "com.android.tools.build:gradle:7.4.2" - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 33 - - defaultConfig { - minSdkVersion 21 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - } - - dependencies { - implementation 'androidx.annotation:annotation:1.0.0' - implementation 'androidx.webkit:webkit:1.7.0' - } - - namespace 'io.flutter.plugins.webviewflutter' -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/settings.gradle b/apps/flutter_parent/plugins/webview_flutter/android/settings.gradle deleted file mode 100644 index 5be7a4b4c6..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'webview_flutter' diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/AndroidManifest.xml b/apps/flutter_parent/plugins/webview_flutter/android/src/main/AndroidManifest.xml deleted file mode 100644 index a087f2c75c..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java deleted file mode 100644 index 1273e73496..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java +++ /dev/null @@ -1,143 +0,0 @@ -package io.flutter.plugins.webviewflutter; - -import static android.hardware.display.DisplayManager.DisplayListener; - -import android.annotation.TargetApi; -import android.hardware.display.DisplayManager; -import android.os.Build; -import android.util.Log; -import java.lang.reflect.Field; -import java.util.ArrayList; - -/** - * Works around an Android WebView bug by filtering some DisplayListener invocations. - * - *

Older Android WebView versions had assumed that when {@link DisplayListener#onDisplayChanged} - * is invoked, the display ID it is provided is of a valid display. However it turns out that when a - * display is removed Android may call onDisplayChanged with the ID of the removed display, in this - * case the Android WebView code tries to fetch and use the display with this ID and crashes with an - * NPE. - * - *

This issue was fixed in the Android WebView code in - * https://chromium-review.googlesource.com/517913 which is available starting WebView version - * 58.0.3029.125 however older webviews in the wild still have this issue. - * - *

Since Flutter removes virtual displays whenever a platform view is resized the webview crash - * is more likely to happen than other apps. And users were reporting this issue see: - * https://github.com/flutter/flutter/issues/30420 - * - *

This class works around the webview bug by unregistering the WebView's DisplayListener, and - * instead registering its own DisplayListener which delegates the callbacks to the WebView's - * listener unless it's a onDisplayChanged for an invalid display. - * - *

I did not find a clean way to get a handle of the WebView's DisplayListener so I'm using - * reflection to fetch all registered listeners before and after initializing a webview. In the - * first initialization of a webview within the process the difference between the lists is the - * webview's display listener. - */ -@TargetApi(Build.VERSION_CODES.KITKAT) -class DisplayListenerProxy { - private static final String TAG = "DisplayListenerProxy"; - - private ArrayList listenersBeforeWebView; - - /** Should be called prior to the webview's initialization. */ - void onPreWebViewInitialization(DisplayManager displayManager) { - listenersBeforeWebView = yoinkDisplayListeners(displayManager); - } - - /** Should be called after the webview's initialization. */ - void onPostWebViewInitialization(final DisplayManager displayManager) { - final ArrayList webViewListeners = yoinkDisplayListeners(displayManager); - // We recorded the list of listeners prior to initializing webview, any new listeners we see - // after initializing the webview are listeners added by the webview. - webViewListeners.removeAll(listenersBeforeWebView); - - if (webViewListeners.isEmpty()) { - // The Android WebView registers a single display listener per process (even if there - // are multiple WebView instances) so this list is expected to be non-empty only the - // first time a webview is initialized. - // Note that in an add2app scenario if the application had instantiated a non Flutter - // WebView prior to instantiating the Flutter WebView we are not able to get a reference - // to the WebView's display listener and can't work around the bug. - // - // This means that webview resizes in add2app Flutter apps with a non Flutter WebView - // running on a system with a webview prior to 58.0.3029.125 may crash (the Android's - // behavior seems to be racy so it doesn't always happen). - return; - } - - for (DisplayListener webViewListener : webViewListeners) { - // Note that while DisplayManager.unregisterDisplayListener throws when given an - // unregistered listener, this isn't an issue as the WebView code never calls - // unregisterDisplayListener. - displayManager.unregisterDisplayListener(webViewListener); - - // We never explicitly unregister this listener as the webview's listener is never - // unregistered (it's released when the process is terminated). - displayManager.registerDisplayListener( - new DisplayListener() { - @Override - public void onDisplayAdded(int displayId) { - for (DisplayListener webViewListener : webViewListeners) { - webViewListener.onDisplayAdded(displayId); - } - } - - @Override - public void onDisplayRemoved(int displayId) { - for (DisplayListener webViewListener : webViewListeners) { - webViewListener.onDisplayRemoved(displayId); - } - } - - @Override - public void onDisplayChanged(int displayId) { - if (displayManager.getDisplay(displayId) == null) { - return; - } - for (DisplayListener webViewListener : webViewListeners) { - webViewListener.onDisplayChanged(displayId); - } - } - }, - null); - } - } - - @SuppressWarnings({"unchecked", "PrivateApi"}) - private static ArrayList yoinkDisplayListeners(DisplayManager displayManager) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - // We cannot use reflection on Android P, but it shouldn't matter as it shipped - // with WebView 66.0.3359.158 and the WebView version the bug this code is working around was - // fixed in 61.0.3116.0. - return new ArrayList<>(); - } - try { - Field displayManagerGlobalField = DisplayManager.class.getDeclaredField("mGlobal"); - displayManagerGlobalField.setAccessible(true); - Object displayManagerGlobal = displayManagerGlobalField.get(displayManager); - Field displayListenersField = - displayManagerGlobal.getClass().getDeclaredField("mDisplayListeners"); - displayListenersField.setAccessible(true); - ArrayList delegates = - (ArrayList) displayListenersField.get(displayManagerGlobal); - - Field listenerField = null; - ArrayList listeners = new ArrayList<>(); - for (Object delegate : delegates) { - if (listenerField == null) { - listenerField = delegate.getClass().getField("mListener"); - listenerField.setAccessible(true); - } - DisplayManager.DisplayListener listener = - (DisplayManager.DisplayListener) listenerField.get(delegate); - listeners.add(listener); - } - return listeners; - } catch (NoSuchFieldException | IllegalAccessException e) { - Log.w(TAG, "Could not extract WebView's display listeners. " + e); - return new ArrayList<>(); - } - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java deleted file mode 100644 index 86b4fd412a..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.webkit.CookieManager; -import android.webkit.ValueCallback; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; - -class FlutterCookieManager implements MethodCallHandler { - private final MethodChannel methodChannel; - - FlutterCookieManager(BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager"); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "clearCookies": - clearCookies(result); - break; - default: - result.notImplemented(); - } - } - - void dispose() { - methodChannel.setMethodCallHandler(null); - } - - private static void clearCookies(final Result result) { - CookieManager cookieManager = CookieManager.getInstance(); - final boolean hasCookies = cookieManager.hasCookies(); - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies( - new ValueCallback() { - @Override - public void onReceiveValue(Boolean value) { - result.success(hasCookies); - } - }); - } else { - cookieManager.removeAllCookie(); - result.success(hasCookies); - } - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java deleted file mode 100644 index cd0e518ea6..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ /dev/null @@ -1,477 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.display.DisplayManager; -import android.os.Build; -import android.os.Handler; -import android.os.Message; -import android.view.View; -import android.webkit.WebChromeClient; -import android.webkit.WebResourceRequest; -import android.webkit.WebStorage; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.webkit.CookieManager; -import androidx.webkit.WebSettingsCompat; -import androidx.webkit.WebViewFeature; -import androidx.annotation.NonNull; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.platform.PlatformView; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import android.util.Log; - - -public class FlutterWebView implements PlatformView, MethodCallHandler { - private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; - private final InputAwareWebView webView; - private final MethodChannel methodChannel; - private final FlutterWebViewClient flutterWebViewClient; - private final Handler platformThreadHandler; - - // Verifies that a url opened by `Window.open` has a secure url. - private class FlutterWebChromeClient extends WebChromeClient { - @Override - public boolean onCreateWindow( - final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { - final WebViewClient webViewClient = - new WebViewClient() { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { - final String url = request.getUrl().toString(); - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, request)) { - webView.loadUrl(url); - } - return true; - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, url)) { - webView.loadUrl(url); - } - return true; - } - }; - - final WebView newWebView = new WebView(view.getContext()); - newWebView.setWebViewClient(webViewClient); - - final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; - transport.setWebView(newWebView); - resultMsg.sendToTarget(); - - return true; - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @SuppressWarnings("unchecked") - FlutterWebView( - final Context context, - BinaryMessenger messenger, - int id, - Map params, - View containerView) { - - DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); - DisplayManager displayManager = - (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); - displayListenerProxy.onPreWebViewInitialization(displayManager); - webView = new InputAwareWebView(context, containerView); - displayListenerProxy.onPostWebViewInitialization(displayManager); - - platformThreadHandler = new Handler(context.getMainLooper()); - // Allow local storage. - webView.getSettings().setDomStorageEnabled(true); - webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); - - // Multi windows is set with FlutterWebChromeClient by default to handle internal bug: b/159892679. - webView.getSettings().setSupportMultipleWindows(true); - webView.setWebChromeClient(new FlutterWebChromeClient()); - - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); - methodChannel.setMethodCallHandler(this); - - flutterWebViewClient = new FlutterWebViewClient(methodChannel); - Map settings = (Map) params.get("settings"); - if (settings != null) applySettings(settings); - - if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { - List names = (List) params.get(JS_CHANNEL_NAMES_FIELD); - if (names != null) registerJavaScriptChannelNames(names); - } - - Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); - if (autoMediaPlaybackPolicy != null) updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); - if (params.containsKey("userAgent")) { - String userAgent = (String) params.get("userAgent"); - updateUserAgent(userAgent); - } - if (params.containsKey("initialUrl")) { - String url = (String) params.get("initialUrl"); - webView.loadUrl(url); - } - } - - @Override - public View getView() { - return webView; - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. - public void onInputConnectionUnlocked() { - webView.unlockInputConnection(); - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. - public void onInputConnectionLocked() { - webView.lockInputConnection(); - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. - public void onFlutterViewAttached(View flutterView) { - webView.setContainerView(flutterView); - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. - public void onFlutterViewDetached() { - webView.setContainerView(null); - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "loadUrl": - loadUrl(methodCall, result); - break; - case "loadData": - loadData(methodCall, result); - break; - case "updateSettings": - updateSettings(methodCall, result); - break; - case "canGoBack": - canGoBack(result); - break; - case "canGoForward": - canGoForward(result); - break; - case "goBack": - goBack(result); - break; - case "goForward": - goForward(result); - break; - case "reload": - reload(result); - break; - case "currentUrl": - currentUrl(result); - break; - case "evaluateJavascript": - evaluateJavaScript(methodCall, result); - break; - case "addJavascriptChannels": - addJavaScriptChannels(methodCall, result); - break; - case "removeJavascriptChannels": - removeJavaScriptChannels(methodCall, result); - break; - case "clearCache": - clearCache(result); - break; - case "getTitle": - getTitle(result); - break; - case "scrollTo": - scrollTo(methodCall, result); - break; - case "scrollBy": - scrollBy(methodCall, result); - break; - case "getScrollX": - getScrollX(result); - break; - case "getScrollY": - getScrollY(result); - break; - case "setAcceptThirdPartyCookies": - setAcceptThirdPartyCookies(methodCall, result); - break; - case "setCookie": - setCookie(methodCall, result); - break; - default: - result.notImplemented(); - } - } - - @SuppressWarnings("unchecked") - private void loadUrl(MethodCall methodCall, Result result) { - Map request = (Map) methodCall.arguments; - String url = (String) request.get("url"); - Map headers = (Map) request.get("headers"); - if (headers == null) { - headers = Collections.emptyMap(); - } - webView.loadUrl(url, headers); - result.success(null); - } - - @SuppressWarnings("unchecked") - private void loadData(MethodCall methodCall, Result result) { - Log.d("FlutterWebView", "Call to load data" + methodCall); - - Map request = (Map) methodCall.arguments; - String baseUrl = (String) request.get("baseUrl"); - String data = (String) request.get("data"); - String mimeType = (String) request.get("mimeType"); - String encoding = (String) request.get("encoding"); - webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, ""); - result.success(null); - } - - private void setAcceptThirdPartyCookies(MethodCall methodCall, Result result) { - boolean accept = (boolean) methodCall.arguments; - CookieManager cookieManager = CookieManager.getInstance(); - cookieManager.setAcceptThirdPartyCookies(webView, accept); - result.success(null); - } - - private void setCookie(MethodCall methodCall, Result result) { - Map args = (Map) methodCall.arguments; - String url = (String) args.get("url"); - String value = (String) args.get("value"); - CookieManager cookieManager = CookieManager.getInstance(); - cookieManager.setCookie(url, value); - result.success(null); - } - - private void canGoBack(Result result) { - result.success(webView.canGoBack()); - } - - private void canGoForward(Result result) { - result.success(webView.canGoForward()); - } - - private void goBack(Result result) { - if (webView.canGoBack()) { - webView.goBack(); - } - result.success(null); - } - - private void goForward(Result result) { - if (webView.canGoForward()) { - webView.goForward(); - } - result.success(null); - } - - private void reload(Result result) { - webView.reload(); - result.success(null); - } - - private void currentUrl(Result result) { - result.success(webView.getUrl()); - } - - @SuppressWarnings("unchecked") - private void updateSettings(MethodCall methodCall, Result result) { - applySettings((Map) methodCall.arguments); - result.success(null); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private void evaluateJavaScript(MethodCall methodCall, final Result result) { - String jsString = (String) methodCall.arguments; - if (jsString == null) { - throw new UnsupportedOperationException("JavaScript string cannot be null"); - } - webView.evaluateJavascript( - jsString, - new android.webkit.ValueCallback() { - @Override - public void onReceiveValue(String value) { - result.success(value); - } - }); - } - - @SuppressWarnings("unchecked") - private void addJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - registerJavaScriptChannelNames(channelNames); - result.success(null); - } - - @SuppressWarnings("unchecked") - private void removeJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - for (String channelName : channelNames) { - webView.removeJavascriptInterface(channelName); - } - result.success(null); - } - - private void clearCache(Result result) { - webView.clearCache(true); - WebStorage.getInstance().deleteAllData(); - result.success(null); - } - - private void getTitle(Result result) { - result.success(webView.getTitle()); - } - - private void scrollTo(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollTo(x, y); - - result.success(null); - } - - private void scrollBy(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollBy(x, y); - result.success(null); - } - - private void getScrollX(Result result) { - result.success(webView.getScrollX()); - } - - private void getScrollY(Result result) { - result.success(webView.getScrollY()); - } - - private void applySettings(Map settings) { - for (String key : settings.keySet()) { - switch (key) { - case "jsMode": - Integer mode = (Integer) settings.get(key); - if (mode != null) updateJsMode(mode); - break; - case "hasNavigationDelegate": - final boolean hasNavigationDelegate = (boolean) settings.get(key); - - final WebViewClient webViewClient = - flutterWebViewClient.createWebViewClient(hasNavigationDelegate); - - webView.setWebViewClient(webViewClient); - break; - case "debuggingEnabled": - final boolean debuggingEnabled = (boolean) settings.get(key); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - webView.setWebContentsDebuggingEnabled(debuggingEnabled); - } - break; - case "gestureNavigationEnabled": - break; - case "userAgent": - updateUserAgent((String) settings.get(key)); - break; - case "darkMode": - setDarkMode((boolean) settings.get(key)); - break; - default: - throw new IllegalArgumentException("Unknown WebView setting: " + key); - } - } - } - - private void setDarkMode(boolean darkMode) { - if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { - WebSettingsCompat.setAlgorithmicDarkeningAllowed(webView.getSettings(), darkMode); - } else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { - int forceDarkMode = darkMode ? WebSettingsCompat.FORCE_DARK_ON : WebSettingsCompat.FORCE_DARK_OFF; - WebSettingsCompat.setForceDark(webView.getSettings(), forceDarkMode); - } else { - Log.d("FlutterWebView", "FORCE_DARK feature is not supported by this WebView"); - } - } - - private void updateJsMode(int mode) { - switch (mode) { - case 0: // disabled - webView.getSettings().setJavaScriptEnabled(false); - break; - case 1: // unrestricted - webView.getSettings().setJavaScriptEnabled(true); - break; - default: - throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode); - } - } - - private void updateAutoMediaPlaybackPolicy(int mode) { - // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all - // other values we require a user gesture. - boolean requireUserGesture = mode != 1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); - } - } - - private void registerJavaScriptChannelNames(List channelNames) { - for (String channelName : channelNames) { - webView.addJavascriptInterface( - new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName); - } - } - - private void updateUserAgent(String userAgent) { - webView.getSettings().setUserAgentString(userAgent); - } - - @Override - public void dispose() { - methodChannel.setMethodCallHandler(null); - webView.dispose(); - webView.destroy(); - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java deleted file mode 100644 index 24926bfc41..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.graphics.Bitmap; -import android.os.Build; -import android.util.Log; -import android.view.KeyEvent; -import android.webkit.WebResourceError; -import android.webkit.WebResourceRequest; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import androidx.annotation.RequiresApi; -import androidx.webkit.WebResourceErrorCompat; -import androidx.webkit.WebViewClientCompat; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -// We need to use WebViewClientCompat to get -// shouldOverrideUrlLoading(WebView view, WebResourceRequest request) -// invoked by the webview on older Android devices, without it pages that use iframes will -// be broken when a navigationDelegate is set on Android version earlier than N. -class FlutterWebViewClient { - private static final String TAG = "FlutterWebViewClient"; - private final MethodChannel methodChannel; - private boolean hasNavigationDelegate; - - FlutterWebViewClient(MethodChannel methodChannel) { - this.methodChannel = methodChannel; - } - - private static String errorCodeToString(int errorCode) { - switch (errorCode) { - case WebViewClient.ERROR_AUTHENTICATION: - return "authentication"; - case WebViewClient.ERROR_BAD_URL: - return "badUrl"; - case WebViewClient.ERROR_CONNECT: - return "connect"; - case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE: - return "failedSslHandshake"; - case WebViewClient.ERROR_FILE: - return "file"; - case WebViewClient.ERROR_FILE_NOT_FOUND: - return "fileNotFound"; - case WebViewClient.ERROR_HOST_LOOKUP: - return "hostLookup"; - case WebViewClient.ERROR_IO: - return "io"; - case WebViewClient.ERROR_PROXY_AUTHENTICATION: - return "proxyAuthentication"; - case WebViewClient.ERROR_REDIRECT_LOOP: - return "redirectLoop"; - case WebViewClient.ERROR_TIMEOUT: - return "timeout"; - case WebViewClient.ERROR_TOO_MANY_REQUESTS: - return "tooManyRequests"; - case WebViewClient.ERROR_UNKNOWN: - return "unknown"; - case WebViewClient.ERROR_UNSAFE_RESOURCE: - return "unsafeResource"; - case WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME: - return "unsupportedAuthScheme"; - case WebViewClient.ERROR_UNSUPPORTED_SCHEME: - return "unsupportedScheme"; - } - - final String message = - String.format(Locale.getDefault(), "Could not find a string for errorCode: %d", errorCode); - throw new IllegalArgumentException(message); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - if (!hasNavigationDelegate) { - return false; - } - notifyOnNavigationRequest( - request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame()); - // We must make a synchronous decision here whether to allow the navigation or not, - // if the Dart code has set a navigation delegate we want that delegate to decide whether - // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we - // return true here to block the navigation, if the Dart delegate decides to allow the - // navigation the plugin will later make an addition loadUrl call for this url. - // - // Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop - // navigations that target the main frame, if the request is not for the main frame - // we just return false to allow the navigation. - // - // For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209 - return request.isForMainFrame(); - } - - boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!hasNavigationDelegate) { - return false; - } - // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with - // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). - // On these devices we cannot tell whether the navigation is targeted to the main frame or not. - // We proceed assuming that the navigation is targeted to the main frame. If the page had any - // frames they will be loaded in the main frame instead. - Log.w( - TAG, - "Using a navigationDelegate with an old webview implementation, pages with frames or iframes will not work"); - notifyOnNavigationRequest(url, null, view, true); - return true; - } - - private void onPageStarted(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageStarted", args); - } - - private void onPageFinished(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageFinished", args); - } - - private void onWebResourceError( - final int errorCode, final String description, final String failingUrl) { - final Map args = new HashMap<>(); - args.put("errorCode", errorCode); - args.put("description", description); - args.put("errorType", FlutterWebViewClient.errorCodeToString(errorCode)); - args.put("failingUrl", failingUrl); - methodChannel.invokeMethod("onWebResourceError", args); - } - - private void notifyOnNavigationRequest( - String url, Map headers, WebView webview, boolean isMainFrame) { - HashMap args = new HashMap<>(); - args.put("url", url); - args.put("isForMainFrame", isMainFrame); - if (isMainFrame) { - methodChannel.invokeMethod( - "navigationRequest", args, new OnNavigationRequestResult(url, headers, webview)); - } else { - methodChannel.invokeMethod("navigationRequest", args); - } - } - - // This method attempts to avoid using WebViewClientCompat due to bug - // https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see - // https://github.com/flutter/flutter/issues/29446. - WebViewClient createWebViewClient(boolean hasNavigationDelegate) { - this.hasNavigationDelegate = hasNavigationDelegate; - - if (!hasNavigationDelegate || android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return internalCreateWebViewClient(); - } - - return internalCreateWebViewClientCompat(); - } - - private WebViewClient internalCreateWebViewClient() { - return new WebViewClient() { - @TargetApi(Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - @TargetApi(Build.VERSION_CODES.M) - @Override - public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceError error) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private WebViewClientCompat internalCreateWebViewClientCompat() { - return new WebViewClientCompat() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, url); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is - // enabled. The deprecated method is called when a device doesn't support this. - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @SuppressLint("RequiresFeature") - @Override - public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceErrorCompat error) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private static class OnNavigationRequestResult implements MethodChannel.Result { - private final String url; - private final Map headers; - private final WebView webView; - - private OnNavigationRequestResult(String url, Map headers, WebView webView) { - this.url = url; - this.headers = headers; - this.webView = webView; - } - - @Override - public void success(Object shouldLoad) { - Boolean typedShouldLoad = (Boolean) shouldLoad; - if (typedShouldLoad) { - loadUrl(); - } - } - - @Override - public void error(String errorCode, String s1, Object o) { - throw new IllegalStateException("navigationRequest calls must succeed"); - } - - @Override - public void notImplemented() { - throw new IllegalStateException( - "navigationRequest must be implemented by the webview method channel"); - } - - private void loadUrl() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - webView.loadUrl(url, headers); - } else { - webView.loadUrl(url); - } - } - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java deleted file mode 100644 index 9b81a5b7cc..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import static android.content.Context.INPUT_METHOD_SERVICE; - -import android.content.Context; -import android.graphics.Rect; -import android.os.Build; -import android.util.Log; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.webkit.WebView; -import android.widget.ListPopupWindow; - -/** - * A WebView subclass that mirrors the same implementation hacks that the system WebView does in - * order to correctly create an InputConnection. - * - *

These hacks are only needed in Android versions below N and exist to create an InputConnection - * on the WebView's dedicated input, or IME, thread. The majority of this proxying logic is in - * {@link #checkInputConnectionProxy}. - * - *

See also {@link ThreadedInputConnectionProxyAdapterView}. - */ -final class InputAwareWebView extends WebView { - private static final String TAG = "InputAwareWebView"; - private View threadedInputConnectionProxyView; - private ThreadedInputConnectionProxyAdapterView proxyAdapterView; - private View containerView; - - InputAwareWebView(Context context, View containerView) { - super(context); - this.containerView = containerView; - } - - void setContainerView(View containerView) { - this.containerView = containerView; - - if (proxyAdapterView == null) { - return; - } - - Log.w(TAG, "The containerView has changed while the proxyAdapterView exists."); - if (containerView != null) { - setInputConnectionTarget(proxyAdapterView); - } - } - - /** - * Set our proxy adapter view to use its cached input connection instead of creating new ones. - * - *

This is used to avoid losing our input connection when the virtual display is resized. - */ - void lockInputConnection() { - if (proxyAdapterView == null) { - return; - } - - proxyAdapterView.setLocked(true); - } - - /** Sets the proxy adapter view back to its default behavior. */ - void unlockInputConnection() { - if (proxyAdapterView == null) { - return; - } - - proxyAdapterView.setLocked(false); - } - - /** Restore the original InputConnection, if needed. */ - void dispose() { - resetInputConnection(); - } - - /** - * Creates an InputConnection from the IME thread when needed. - * - *

We only need to create a {@link ThreadedInputConnectionProxyAdapterView} and create an - * InputConnectionProxy on the IME thread when WebView is doing the same thing. So we rely on the - * system calling this method for WebView's proxy view in order to know when we need to create our - * own. - * - *

This method would normally be called for any View that used the InputMethodManager. We rely - * on flutter/engine filtering the calls we receive down to the ones in our hierarchy and the - * system WebView in order to know whether or not the system WebView expects an InputConnection on - * the IME thread. - */ - @Override - public boolean checkInputConnectionProxy(final View view) { - // Check to see if the view param is WebView's ThreadedInputConnectionProxyView. - View previousProxy = threadedInputConnectionProxyView; - threadedInputConnectionProxyView = view; - if (previousProxy == view) { - // This isn't a new ThreadedInputConnectionProxyView. Ignore it. - return super.checkInputConnectionProxy(view); - } - if (containerView == null) { - Log.e( - TAG, - "Can't create a proxy view because there's no container view. Text input may not work."); - return super.checkInputConnectionProxy(view); - } - - // We've never seen this before, so we make the assumption that this is WebView's - // ThreadedInputConnectionProxyView. We are making the assumption that the only view that could - // possibly be interacting with the IMM here is WebView's ThreadedInputConnectionProxyView. - proxyAdapterView = - new ThreadedInputConnectionProxyAdapterView( - /*containerView=*/ containerView, - /*targetView=*/ view, - /*imeHandler=*/ view.getHandler()); - setInputConnectionTarget(/*targetView=*/ proxyAdapterView); - return super.checkInputConnectionProxy(view); - } - - /** - * Ensure that input creation happens back on {@link #containerView}'s thread once this view no - * longer has focus. - * - *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's - * thread for all connections. We undo it here so users will be able to go back to typing in - * Flutter UIs as expected. - */ - @Override - public void clearFocus() { - super.clearFocus(); - resetInputConnection(); - } - - /** - * Ensure that input creation happens back on {@link #containerView}. - * - *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's - * thread for all connections. We undo it here so users will be able to go back to typing in - * Flutter UIs as expected. - */ - private void resetInputConnection() { - if (proxyAdapterView == null) { - // No need to reset the InputConnection to the default thread if we've never changed it. - return; - } - if (containerView == null) { - Log.e(TAG, "Can't reset the input connection to the container view because there is none."); - return; - } - setInputConnectionTarget(/*targetView=*/ containerView); - } - - /** - * This is the crucial trick that gets the InputConnection creation to happen on the correct - * thread pre Android N. - * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java?l=169&rcl=f0698ee3e4483fad5b0c34159276f71cfaf81f3a - * - *

{@code targetView} should have a {@link View#getHandler} method with the thread that future - * InputConnections should be created on. - */ - private void setInputConnectionTarget(final View targetView) { - if (containerView == null) { - Log.e( - TAG, - "Can't set the input connection target because there is no containerView to use as a handler."); - return; - } - - targetView.requestFocus(); - containerView.post( - new Runnable() { - @Override - public void run() { - InputMethodManager imm = - (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); - // This is a hack to make InputMethodManager believe that the target view now has focus. - // As a result, InputMethodManager will think that targetView is focused, and will call - // getHandler() of the view when creating input connection. - - // Step 1: Set targetView as InputMethodManager#mNextServedView. This does not affect - // the real window focus. - targetView.onWindowFocusChanged(true); - - // Step 2: Have InputMethodManager focus in on targetView. As a result, IMM will call - // onCreateInputConnection() on targetView on the same thread as - // targetView.getHandler(). It will also call subsequent InputConnection methods on this - // thread. This is the IME thread in cases where targetView is our proxyAdapterView. - imm.isActive(containerView); - } - }); - } - - @Override - protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { - // This works around a crash when old (<67.0.3367.0) Chromium versions are used. - - // Prior to Chromium 67.0.3367 the following sequence happens when a select drop down is shown - // on tablets: - // - // - WebView is calling ListPopupWindow#show - // - buildDropDown is invoked, which sets mDropDownList to a DropDownListView. - // - showAsDropDown is invoked - resulting in mDropDownList being added to the window and is - // also synchronously performing the following sequence: - // - WebView's focus change listener is loosing focus (as mDropDownList got it) - // - WebView is hiding all popups (as it lost focus) - // - WebView's SelectPopupDropDown#hide is invoked. - // - DropDownPopupWindow#dismiss is invoked setting mDropDownList to null. - // - mDropDownList#setSelection is invoked and is throwing a NullPointerException (as we just set mDropDownList to null). - // - // To workaround this, we drop the problematic focus lost call. - // See more details on: https://github.com/flutter/flutter/issues/54164 - // - // We don't do this after Android P as it shipped with a new enough WebView version, and it's - // better to not do this on all future Android versions in case DropDownListView's code changes. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P - && isCalledFromListPopupWindowShow() - && !focused) { - return; - } - super.onFocusChanged(focused, direction, previouslyFocusedRect); - } - - private boolean isCalledFromListPopupWindowShow() { - StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); - for (StackTraceElement stackTraceElement : stackTraceElements) { - if (stackTraceElement.getClassName().equals(ListPopupWindow.class.getCanonicalName()) - && stackTraceElement.getMethodName().equals("show")) { - return true; - } - } - return false; - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java deleted file mode 100644 index f23aae5b2b..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.os.Handler; -import android.os.Looper; -import android.webkit.JavascriptInterface; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; - -/** - * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets - * up. - * - *

Exposes a single method named `postMessage` to JavaScript, which sends a message over a method - * channel to the Dart code. - */ -class JavaScriptChannel { - private final MethodChannel methodChannel; - private final String javaScriptChannelName; - private final Handler platformThreadHandler; - - /** - * @param methodChannel the Flutter WebView method channel to which JS messages are sent - * @param javaScriptChannelName the name of the JavaScript channel, this is sent over the method - * channel with each message to let the Dart code know which JavaScript channel the message - * was sent through - */ - JavaScriptChannel( - MethodChannel methodChannel, String javaScriptChannelName, Handler platformThreadHandler) { - this.methodChannel = methodChannel; - this.javaScriptChannelName = javaScriptChannelName; - this.platformThreadHandler = platformThreadHandler; - } - - // Suppressing unused warning as this is invoked from JavaScript. - @SuppressWarnings("unused") - @JavascriptInterface - public void postMessage(final String message) { - Runnable postMessageRunnable = - new Runnable() { - @Override - public void run() { - HashMap arguments = new HashMap<>(); - arguments.put("channel", javaScriptChannelName); - arguments.put("message", message); - methodChannel.invokeMethod("javascriptChannelMessage", arguments); - } - }; - if (platformThreadHandler.getLooper() == Looper.myLooper()) { - postMessageRunnable.run(); - } else { - platformThreadHandler.post(postMessageRunnable); - } - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java deleted file mode 100644 index 8fbdfaff1a..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.os.Handler; -import android.os.IBinder; -import android.view.View; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; - -/** - * A fake View only exposed to InputMethodManager. - * - *

This follows a similar flow to Chromium's WebView (see - * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionProxyView.java). - * WebView itself bounces its InputConnection around several different threads. We follow its logic - * here to get the same working connection. - * - *

This exists solely to forward input creation to WebView's ThreadedInputConnectionProxyView on - * the IME thread. The way that this is created in {@link - * InputAwareWebView#checkInputConnectionProxy} guarantees that we have a handle to - * ThreadedInputConnectionProxyView and {@link #onCreateInputConnection} is always called on the IME - * thread. We delegate to ThreadedInputConnectionProxyView there to get WebView's input connection. - */ -final class ThreadedInputConnectionProxyAdapterView extends View { - final Handler imeHandler; - final IBinder windowToken; - final View containerView; - final View rootView; - final View targetView; - - private boolean triggerDelayed = true; - private boolean isLocked = false; - private InputConnection cachedConnection; - - ThreadedInputConnectionProxyAdapterView(View containerView, View targetView, Handler imeHandler) { - super(containerView.getContext()); - this.imeHandler = imeHandler; - this.containerView = containerView; - this.targetView = targetView; - windowToken = containerView.getWindowToken(); - rootView = containerView.getRootView(); - setFocusable(true); - setFocusableInTouchMode(true); - setVisibility(VISIBLE); - } - - /** Returns whether or not this is currently asynchronously acquiring an input connection. */ - boolean isTriggerDelayed() { - return triggerDelayed; - } - - /** Sets whether or not this should use its previously cached input connection. */ - void setLocked(boolean locked) { - isLocked = locked; - } - - /** - * This is expected to be called on the IME thread. See the setup required for this in {@link - * InputAwareWebView#checkInputConnectionProxy(View)}. - * - *

Delegates to ThreadedInputConnectionProxyView to get WebView's input connection. - */ - @Override - public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { - triggerDelayed = false; - InputConnection inputConnection = - (isLocked) ? cachedConnection : targetView.onCreateInputConnection(outAttrs); - triggerDelayed = true; - cachedConnection = inputConnection; - return inputConnection; - } - - @Override - public boolean checkInputConnectionProxy(View view) { - return true; - } - - @Override - public boolean hasWindowFocus() { - // None of our views here correctly report they have window focus because of how we're embedding - // the platform view inside of a virtual display. - return true; - } - - @Override - public View getRootView() { - return rootView; - } - - @Override - public boolean onCheckIsTextEditor() { - return true; - } - - @Override - public boolean isFocused() { - return true; - } - - @Override - public IBinder getWindowToken() { - return windowToken; - } - - @Override - public Handler getHandler() { - return imeHandler; - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java deleted file mode 100644 index 6fdc36fbe5..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.content.Context; -import android.view.View; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.StandardMessageCodec; -import io.flutter.plugin.platform.PlatformView; -import io.flutter.plugin.platform.PlatformViewFactory; -import java.util.Map; - -public final class WebViewFactory extends PlatformViewFactory { - private final BinaryMessenger messenger; - private final View containerView; - - WebViewFactory(BinaryMessenger messenger, View containerView) { - super(StandardMessageCodec.INSTANCE); - this.messenger = messenger; - this.containerView = containerView; - } - - @SuppressWarnings("unchecked") - @Override - public PlatformView create(Context context, int id, Object args) { - Map params = (Map) args; - return new FlutterWebView(context, messenger, id, params, containerView); - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java deleted file mode 100644 index 2de8fdf94b..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; - -/** - * Java platform implementation of the webview_flutter plugin. - * - *

Register this in an add to app scenario to gracefully handle activity and context changes. - * - *

Call {@link #registerWith(Registrar)} to use the stable {@code io.flutter.plugin.common} - * package instead. - */ -public class WebViewFlutterPlugin implements FlutterPlugin { - - private FlutterCookieManager flutterCookieManager; - - /** - * Add an instance of this to {@link io.flutter.embedding.engine.plugins.PluginRegistry} to - * register it. - * - *

THIS PLUGIN CODE PATH DEPENDS ON A NEWER VERSION OF FLUTTER THAN THE ONE DEFINED IN THE - * PUBSPEC.YAML. Text input will fail on some Android devices unless this is used with at least - * flutter/flutter@1d4d63ace1f801a022ea9ec737bf8c15395588b9. Use the V1 embedding with {@link - * #registerWith(Registrar)} to use this plugin with older Flutter versions. - * - *

Registration should eventually be handled automatically by v2 of the - * GeneratedPluginRegistrant. https://github.com/flutter/flutter/issues/42694 - */ - public WebViewFlutterPlugin() {} - - /** - * Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common} - * package. - * - *

Calling this automatically initializes the plugin. However plugins initialized this way - * won't react to changes in activity or context, unlike {@link CameraPlugin}. - */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - registrar - .platformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", - new WebViewFactory(registrar.messenger(), registrar.view())); - new FlutterCookieManager(registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - BinaryMessenger messenger = binding.getBinaryMessenger(); - binding - .getPlatformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", new WebViewFactory(messenger, /*containerView=*/ null)); - flutterCookieManager = new FlutterCookieManager(messenger); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - if (flutterCookieManager == null) { - return; - } - - flutterCookieManager.dispose(); - flutterCookieManager = null; - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/platform_interface.dart b/apps/flutter_parent/plugins/webview_flutter/lib/platform_interface.dart deleted file mode 100644 index 2637e0d9f0..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/lib/platform_interface.dart +++ /dev/null @@ -1,549 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/widgets.dart'; - -import 'webview_flutter.dart'; - -/// Interface for callbacks made by [WebViewPlatformController]. -/// -/// The webview plugin implements this class, and passes an instance to the [WebViewPlatformController]. -/// [WebViewPlatformController] is notifying this handler on events that happened on the platform's webview. -abstract class WebViewPlatformCallbacksHandler { - /// Invoked by [WebViewPlatformController] when a JavaScript channel message is received. - void onJavaScriptChannelMessage(String channel, String message); - - /// Invoked by [WebViewPlatformController] when a navigation request is pending. - /// - /// If true is returned the navigation is allowed, otherwise it is blocked. - FutureOr onNavigationRequest({String url, bool isForMainFrame}); - - /// Invoked by [WebViewPlatformController] when a page has started loading. - void onPageStarted(String url); - - /// Invoked by [WebViewPlatformController] when a page has finished loading. - void onPageFinished(String url); - - /// Report web resource loading error to the host application. - void onWebResourceError(WebResourceError error); -} - -/// Possible error type categorizations used by [WebResourceError]. -enum WebResourceErrorType { - /// User authentication failed on server. - authentication, - - /// Malformed URL. - badUrl, - - /// Failed to connect to the server. - connect, - - /// Failed to perform SSL handshake. - failedSslHandshake, - - /// Generic file error. - file, - - /// File not found. - fileNotFound, - - /// Server or proxy hostname lookup failed. - hostLookup, - - /// Failed to read or write to the server. - io, - - /// User authentication failed on proxy. - proxyAuthentication, - - /// Too many redirects. - redirectLoop, - - /// Connection timed out. - timeout, - - /// Too many requests during this load. - tooManyRequests, - - /// Generic error. - unknown, - - /// Resource load was canceled by Safe Browsing. - unsafeResource, - - /// Unsupported authentication scheme (not basic or digest). - unsupportedAuthScheme, - - /// Unsupported URI scheme. - unsupportedScheme, - - /// The web content process was terminated. - webContentProcessTerminated, - - /// The web view was invalidated. - webViewInvalidated, - - /// A JavaScript exception occurred. - javaScriptExceptionOccurred, - - /// The result of JavaScript execution could not be returned. - javaScriptResultTypeIsUnsupported, -} - -/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. -class WebResourceError { - /// Creates a new [WebResourceError] - /// - /// A user should not need to instantiate this class, but will receive one in - /// [WebResourceErrorCallback]. - WebResourceError({ - required this.errorCode, - required this.description, - this.domain, - this.errorType, - this.failingUrl, - }) {} - - /// Raw code of the error from the respective platform. - /// - /// On Android, the error code will be a constant from a - /// [WebViewClient](https://developer.android.com/reference/android/webkit/WebViewClient#summary) and - /// will have a corresponding [errorType]. - /// - /// On iOS, the error code will be a constant from `NSError.code` in - /// Objective-C. See - /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html - /// for more information on error handling on iOS. Some possible error codes - /// can be found at https://developer.apple.com/documentation/webkit/wkerrorcode?language=objc. - final int errorCode; - - /// The domain of where to find the error code. - /// - /// This field is only available on iOS and represents a "domain" from where - /// the [errorCode] is from. This value is taken directly from an `NSError` - /// in Objective-C. See - /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html - /// for more information on error handling on iOS. - final String? domain; - - /// Description of the error that can be used to communicate the problem to the user. - final String description; - - /// The type this error can be categorized as. - /// - /// This will never be `null` on Android, but can be `null` on iOS. - final WebResourceErrorType? errorType; - - /// Gets the URL for which the resource request was made. - /// - /// This value is not provided on iOS. Alternatively, you can keep track of - /// the last values provided to [WebViewPlatformController.loadUrl]. - final String? failingUrl; -} - -/// Interface for talking to the webview's platform implementation. -/// -/// An instance implementing this interface is passed to the `onWebViewPlatformCreated` callback that is -/// passed to [WebViewPlatformBuilder#onWebViewPlatformCreated]. -/// -/// Platform implementations that live in a separate package should extend this class rather than -/// implement it as webview_flutter does not consider newly added methods to be breaking changes. -/// Extending this class (using `extends`) ensures that the subclass will get the default -/// implementation, while platform implementations that `implements` this interface will be broken -/// by newly added [WebViewPlatformController] methods. -abstract class WebViewPlatformController { - /// Creates a new WebViewPlatform. - /// - /// Callbacks made by the WebView will be delegated to `handler`. - /// - /// The `handler` parameter must not be null. - WebViewPlatformController(WebViewPlatformCallbacksHandler handler); - - /// Loads the specified URL. - /// - /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will - /// be added as key value pairs of HTTP headers for the request. - /// - /// `url` must not be null. - /// - /// Throws an ArgumentError if `url` is not a valid URL string. - Future loadUrl( - String url, - Map? headers, - ) { - throw UnimplementedError( - "WebView loadUrl is not implemented on the current platform"); - } - - Future loadData( - String? baseUrl, - String data, - String mimeType, - String encoding - ) { - throw UnimplementedError( - "WebView loadData is not implemented on the current platform"); - } - - Future setAcceptThirdPartyCookies(bool accept) { - throw UnimplementedError("WebView setAcceptThirdPartyCookies is not implemented on the current platform"); - } - - Future setCookie(String url, String value) { - throw UnimplementedError("WebView setCookie is not implemented on the current platform"); - } - - /// Updates the webview settings. - /// - /// Any non null field in `settings` will be set as the new setting value. - /// All null fields in `settings` are ignored. - Future updateSettings(WebSettings setting) { - throw UnimplementedError( - "WebView updateSettings is not implemented on the current platform"); - } - - /// Accessor to the current URL that the WebView is displaying. - /// - /// If no URL was ever loaded, returns `null`. - Future currentUrl() { - throw UnimplementedError( - "WebView currentUrl is not implemented on the current platform"); - } - - /// Checks whether there's a back history item. - Future canGoBack() { - throw UnimplementedError( - "WebView canGoBack is not implemented on the current platform"); - } - - /// Checks whether there's a forward history item. - Future canGoForward() { - throw UnimplementedError( - "WebView canGoForward is not implemented on the current platform"); - } - - /// Goes back in the history of this WebView. - /// - /// If there is no back history item this is a no-op. - Future goBack() { - throw UnimplementedError( - "WebView goBack is not implemented on the current platform"); - } - - /// Goes forward in the history of this WebView. - /// - /// If there is no forward history item this is a no-op. - Future goForward() { - throw UnimplementedError( - "WebView goForward is not implemented on the current platform"); - } - - /// Reloads the current URL. - Future reload() { - throw UnimplementedError( - "WebView reload is not implemented on the current platform"); - } - - /// Clears all caches used by the [WebView]. - /// - /// The following caches are cleared: - /// 1. Browser HTTP Cache. - /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. - /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. - /// 3. Application cache. - /// 4. Local Storage. - Future clearCache() { - throw UnimplementedError( - "WebView clearCache is not implemented on the current platform"); - } - - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// The Future completes with an error if a JavaScript error occurred, or if the type of the - /// evaluated expression is not supported(e.g on iOS not all non primitive type can be evaluated). - Future evaluateJavascript(String javascriptString) { - throw UnimplementedError( - "WebView evaluateJavascript is not implemented on the current platform"); - } - - /// Adds new JavaScript channels to the set of enabled channels. - /// - /// For each value in this list the platform's webview should make sure that a corresponding - /// property with a postMessage method is set on `window`. For example for a JavaScript channel - /// named `Foo` it should be possible for JavaScript code executing in the webview to do - /// - /// ```javascript - /// Foo.postMessage('hello'); - /// ``` - /// - /// See also: [CreationParams.javascriptChannelNames]. - Future addJavascriptChannels(Set javascriptChannelNames) { - throw UnimplementedError( - "WebView addJavascriptChannels is not implemented on the current platform"); - } - - /// Removes JavaScript channel names from the set of enabled channels. - /// - /// This disables channels that were previously enabled by [addJavaScriptChannels] or through - /// [CreationParams.javascriptChannelNames]. - Future removeJavascriptChannels(Set javascriptChannelNames) { - throw UnimplementedError( - "WebView removeJavascriptChannels is not implemented on the current platform"); - } - - /// Returns the title of the currently loaded page. - Future getTitle() { - throw UnimplementedError( - "WebView getTitle is not implemented on the current platform"); - } - - /// Set the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the position to scroll to in WebView pixels. - Future scrollTo(int x, int y) { - throw UnimplementedError( - "WebView scrollTo is not implemented on the current platform"); - } - - /// Move the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by. - Future scrollBy(int x, int y) { - throw UnimplementedError( - "WebView scrollBy is not implemented on the current platform"); - } - - /// Return the horizontal scroll position of this view. - /// - /// Scroll position is measured from left. - Future getScrollX() { - throw UnimplementedError( - "WebView getScrollX is not implemented on the current platform"); - } - - /// Return the vertical scroll position of this view. - /// - /// Scroll position is measured from top. - Future getScrollY() { - throw UnimplementedError( - "WebView getScrollY is not implemented on the current platform"); - } -} - -/// A single setting for configuring a WebViewPlatform which may be absent. -class WebSetting { - /// Constructs an absent setting instance. - /// - /// The [isPresent] field for the instance will be false. - /// - /// Accessing [value] for an absent instance will throw. - WebSetting.absent() - : _value = null, - isPresent = false; - - /// Constructs a setting of the given `value`. - /// - /// The [isPresent] field for the instance will be true. - WebSetting.of(T value) - : _value = value, - isPresent = true; - - final T? _value; - - /// The setting's value. - /// - /// Throws if [WebSetting.isPresent] is false. - T get value { - if (!isPresent) { - throw StateError('Cannot access a value of an absent WebSetting'); - } - if (_value == null) { - throw StateError('Cannot access a value of an absent WebSetting'); - } - assert(isPresent); - return _value!; - } - - /// True when this web setting instance contains a value. - /// - /// When false the [WebSetting.value] getter throws. - final bool isPresent; - - @override - bool operator ==(other) { - if (other.runtimeType != runtimeType) return false; - if (other is! WebSetting) return false; - final WebSetting typedOther = other; - return typedOther.isPresent == isPresent && typedOther._value == _value; - } - - @override - int get hashCode => hashValues(_value, isPresent); -} - -/// Settings for configuring a WebViewPlatform. -/// -/// Initial settings are passed as part of [CreationParams], settings updates are sent with -/// [WebViewPlatform#updateSettings]. -/// -/// The `userAgent` parameter must not be null. -class WebSettings { - /// Construct an instance with initial settings. Future setting changes can be - /// sent with [WebviewPlatform#updateSettings]. - /// - /// The `userAgent` parameter must not be null. - WebSettings({ - this.javascriptMode, - this.hasNavigationDelegate, - this.darkMode, - this.debuggingEnabled, - this.gestureNavigationEnabled, - required this.userAgent, - }) {} - - /// The JavaScript execution mode to be used by the webview. - final JavascriptMode? javascriptMode; - - /// Whether the [WebView] has a [NavigationDelegate] set. - final bool? hasNavigationDelegate; - - final bool? darkMode; - - /// Whether to enable the platform's webview content debugging tools. - /// - /// See also: [WebView.debuggingEnabled]. - final bool? debuggingEnabled; - - /// The value used for the HTTP `User-Agent:` request header. - /// - /// If [userAgent.value] is null the platform's default user agent should be used. - /// - /// An absent value ([userAgent.isPresent] is false) represents no change to this setting from the - /// last time it was set. - /// - /// See also [WebView.userAgent]. - final WebSetting userAgent; - - /// Whether to allow swipe based navigation in iOS. - /// - /// See also: [WebView.gestureNavigationEnabled] - final bool? gestureNavigationEnabled; - - @override - String toString() { - return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent)'; - } -} - -/// Configuration to use when creating a new [WebViewPlatformController]. -/// -/// The `autoMediaPlaybackPolicy` parameter must not be null. -class CreationParams { - /// Constructs an instance to use when creating a new - /// [WebViewPlatformController]. - /// - /// The `autoMediaPlaybackPolicy` parameter must not be null. - CreationParams({ - this.initialUrl, - this.webSettings, - this.javascriptChannelNames, - this.userAgent, - this.autoMediaPlaybackPolicy = - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - }) {} - - /// The initialUrl to load in the webview. - /// - /// When null the webview will be created without loading any page. - final String? initialUrl; - - /// The initial [WebSettings] for the new webview. - /// - /// This can later be updated with [WebViewPlatformController.updateSettings]. - final WebSettings? webSettings; - - /// The initial set of JavaScript channels that are configured for this webview. - /// - /// For each value in this set the platform's webview should make sure that a corresponding - /// property with a postMessage method is set on `window`. For example for a JavaScript channel - /// named `Foo` it should be possible for JavaScript code executing in the webview to do - /// - /// ```javascript - /// Foo.postMessage('hello'); - /// ``` - // TODO(amirh): describe what should happen when postMessage is called once that code is migrated - // to PlatformWebView. - final Set? javascriptChannelNames; - - /// The value used for the HTTP User-Agent: request header. - /// - /// When null the platform's webview default is used for the User-Agent header. - final String? userAgent; - - /// Which restrictions apply on automatic media playback. - final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy; - - @override - String toString() { - return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent)'; - } -} - -/// Signature for callbacks reporting that a [WebViewPlatformController] was created. -/// -/// See also the `onWebViewPlatformCreated` argument for [WebViewPlatform.build]. -typedef WebViewPlatformCreatedCallback = void Function( - WebViewPlatformController webViewPlatformController); - -/// Interface for a platform implementation of a WebView. -/// -/// [WebView.platform] controls the builder that is used by [WebView]. -/// [AndroidWebViewPlatform] and [CupertinoWebViewPlatform] are the default implementations -/// for Android and iOS respectively. -abstract class WebViewPlatform { - /// Builds a new WebView. - /// - /// Returns a Widget tree that embeds the created webview. - /// - /// `creationParams` are the initial parameters used to setup the webview. - /// - /// `webViewPlatformHandler` will be used for handling callbacks that are made by the created - /// [WebViewPlatformController]. - /// - /// `onWebViewPlatformCreated` will be invoked after the platform specific [WebViewPlatformController] - /// implementation is created with the [WebViewPlatformController] instance as a parameter. - /// - /// `gestureRecognizers` specifies which gestures should be consumed by the web view. - /// It is possible for other gesture recognizers to be competing with the web view on pointer - /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle - /// vertical drags. The web view will claim gestures that are recognized by any of the - /// recognizers on this list. - /// When `gestureRecognizers` is empty or null, the web view will only handle pointer events for gestures that - /// were not claimed by any other gesture recognizer. - /// - /// `webViewPlatformHandler` must not be null. - Widget build({ - BuildContext context, - // TODO(amirh): convert this to be the actual parameters. - // I'm starting without it as the PR is starting to become pretty big. - // I'll followup with the conversion PR. - CreationParams creationParams, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set>? gestureRecognizers, - }); - - /// Clears all cookies for all [WebView] instances. - /// - /// Returns true if cookies were present before clearing, else false. - Future clearCookies() { - throw UnimplementedError( - "WebView clearCookies is not implemented on the current platform"); - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_android.dart b/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_android.dart deleted file mode 100644 index f987f5b67f..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_android.dart +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import '../platform_interface.dart'; -import 'webview_method_channel.dart'; - -/// Builds an Android webview. -/// -/// This is used as the default implementation for [WebView.platform] on Android. It uses -/// an [AndroidView] to embed the webview in the widget hierarchy, and uses a method channel to -/// communicate with the platform code. -class AndroidWebView implements WebViewPlatform { - @override - Widget build({ - BuildContext? context, - CreationParams? creationParams, - @required WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }) { - assert(webViewPlatformCallbacksHandler != null); - return GestureDetector( - // We prevent text selection by intercepting the long press event. - // This is a temporary stop gap due to issues with text selection on Android: - // https://github.com/flutter/flutter/issues/24585 - the text selection - // dialog is not responding to touch events. - // https://github.com/flutter/flutter/issues/24584 - the text selection - // handles are not showing. - // TODO(amirh): remove this when the issues above are fixed. - onLongPress: () {}, - excludeFromSemantics: true, - child: AndroidView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null || webViewPlatformCallbacksHandler == null) { - return; - } - onWebViewPlatformCreated(MethodChannelWebViewPlatform( - id, webViewPlatformCallbacksHandler)); - }, - gestureRecognizers: gestureRecognizers, - // WebView content is not affected by the Android view's layout direction, - // we explicitly set it here so that the widget doesn't require an ambient - // directionality. - layoutDirection: TextDirection.rtl, - creationParams: - creationParams != null ? MethodChannelWebViewPlatform.creationParamsToMap(creationParams) : null, - creationParamsCodec: const StandardMessageCodec(), - ), - ); - } - - @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); -} diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_cupertino.dart b/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_cupertino.dart deleted file mode 100644 index 52e7b823fc..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_cupertino.dart +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import '../platform_interface.dart'; -import 'webview_method_channel.dart'; - -/// Builds an iOS webview. -/// -/// This is used as the default implementation for [WebView.platform] on iOS. It uses -/// a [UiKitView] to embed the webview in the widget hierarchy, and uses a method channel to -/// communicate with the platform code. -class CupertinoWebView implements WebViewPlatform { - @override - Widget build({ - BuildContext? context, - CreationParams? creationParams, - @required WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }) { - return UiKitView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null || webViewPlatformCallbacksHandler == null) { - return; - } - onWebViewPlatformCreated( - MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler)); - }, - gestureRecognizers: gestureRecognizers, - creationParams: - creationParams != null ? MethodChannelWebViewPlatform.creationParamsToMap(creationParams) : null, - creationParamsCodec: const StandardMessageCodec(), - ); - } - - @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); -} diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_method_channel.dart b/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_method_channel.dart deleted file mode 100644 index 7d7e232ff8..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/lib/src/webview_method_channel.dart +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; - -import '../platform_interface.dart'; - -/// A [WebViewPlatformController] that uses a method channel to control the webview. -class MethodChannelWebViewPlatform implements WebViewPlatformController { - /// Constructs an instance that will listen for webviews broadcasting to the - /// given [id], using the given [WebViewPlatformCallbacksHandler]. - MethodChannelWebViewPlatform(int id, this._platformCallbacksHandler) - : _channel = MethodChannel('plugins.flutter.io/webview_$id') { - _channel.setMethodCallHandler(_onMethodCall); - } - - final WebViewPlatformCallbacksHandler _platformCallbacksHandler; - - final MethodChannel _channel; - - static const MethodChannel _cookieManagerChannel = - MethodChannel('plugins.flutter.io/cookie_manager'); - - Future _onMethodCall(MethodCall call) async { - switch (call.method) { - case 'javascriptChannelMessage': - final String channel = call.arguments['channel']; - final String message = call.arguments['message']; - _platformCallbacksHandler.onJavaScriptChannelMessage(channel, message); - return true; - case 'navigationRequest': - return await _platformCallbacksHandler.onNavigationRequest( - url: call.arguments['url'], - isForMainFrame: call.arguments['isForMainFrame'], - ); - case 'onPageFinished': - _platformCallbacksHandler.onPageFinished(call.arguments['url']); - return null; - case 'onPageStarted': - _platformCallbacksHandler.onPageStarted(call.arguments['url']); - return null; - case 'onWebResourceError': - _platformCallbacksHandler.onWebResourceError( - WebResourceError( - errorCode: call.arguments['errorCode'], - description: call.arguments['description'], - domain: call.arguments['domain'], - failingUrl: call.arguments['failingUrl'], - errorType: call.arguments['errorType'] == null - ? WebResourceErrorType.unknown - : WebResourceErrorType.values.firstWhere( - (WebResourceErrorType type) { - return type.toString() == - '$WebResourceErrorType.${call.arguments['errorType']}'; - }, - ), - ), - ); - return null; - } - - throw MissingPluginException( - '${call.method} was invoked but has no handler', - ); - } - - @override - Future loadUrl( - String url, - Map? headers, - ) async { - return _channel.invokeMethod('loadUrl', { - 'url': url, - 'headers': headers, - }); - } - - @override - Future loadData(String? baseUrl, String data, String mimeType, String encoding) async { - return _channel.invokeMethod('loadData', { - 'baseUrl': baseUrl, - 'data': data, - 'mimeType': mimeType, - 'encoding': encoding, - }); - } - - @override - Future setAcceptThirdPartyCookies(bool accept) { - return _channel.invokeMethod('setAcceptThirdPartyCookies', accept); - } - - @override - Future setCookie(String url, String value) { - return _channel.invokeMethod( - 'setCookie', - { - 'url': url, - 'value': value, - }, - ); - } - - @override - Future currentUrl() => _channel.invokeMethod('currentUrl'); - - @override - Future canGoBack() => _channel.invokeMethod("canGoBack"); - - @override - Future canGoForward() => _channel.invokeMethod("canGoForward"); - - @override - Future goBack() => _channel.invokeMethod("goBack"); - - @override - Future goForward() => _channel.invokeMethod("goForward"); - - @override - Future reload() => _channel.invokeMethod("reload"); - - @override - Future clearCache() => _channel.invokeMethod("clearCache"); - - @override - Future updateSettings(WebSettings settings) { - final Map updatesMap = _webSettingsToMap(settings); - if (updatesMap.isEmpty) { - return Future.value(); - } - return _channel.invokeMethod('updateSettings', updatesMap); - } - - @override - Future evaluateJavascript(String javascriptString) { - return _channel.invokeMethod('evaluateJavascript', javascriptString); - } - - @override - Future addJavascriptChannels(Set javascriptChannelNames) { - return _channel.invokeMethod( - 'addJavascriptChannels', javascriptChannelNames.toList()); - } - - @override - Future removeJavascriptChannels(Set javascriptChannelNames) { - return _channel.invokeMethod( - 'removeJavascriptChannels', javascriptChannelNames.toList()); - } - - @override - Future getTitle() => _channel.invokeMethod("getTitle"); - - @override - Future scrollTo(int x, int y) { - return _channel.invokeMethod('scrollTo', { - 'x': x, - 'y': y, - }); - } - - @override - Future scrollBy(int x, int y) { - return _channel.invokeMethod('scrollBy', { - 'x': x, - 'y': y, - }); - } - - @override - Future getScrollX() => _channel.invokeMethod("getScrollX"); - - @override - Future getScrollY() => _channel.invokeMethod("getScrollY"); - - /// Method channel implementation for [WebViewPlatform.clearCookies]. - static Future clearCookies() { - return _cookieManagerChannel - .invokeMethod('clearCookies') - .then((dynamic result) => result); - } - - static Map _webSettingsToMap(WebSettings? settings) { - final Map map = {}; - void _addIfNonNull(String key, dynamic value) { - if (value == null) { - return; - } - map[key] = value; - } - - void _addSettingIfPresent(String key, WebSetting setting) { - if (!setting.isPresent) { - return; - } - map[key] = setting.value; - } - - _addIfNonNull('jsMode', settings?.javascriptMode?.index); - _addIfNonNull('hasNavigationDelegate', settings?.hasNavigationDelegate); - _addIfNonNull('darkMode', settings?.darkMode); - _addIfNonNull('debuggingEnabled', settings?.debuggingEnabled); - _addIfNonNull( - 'gestureNavigationEnabled', settings?.gestureNavigationEnabled); - _addSettingIfPresent('userAgent', settings?.userAgent as WebSetting); - return map; - } - - /// Converts a [CreationParams] object to a map as expected by `platform_views` channel. - /// - /// This is used for the `creationParams` argument of the platform views created by - /// [AndroidWebViewBuilder] and [CupertinoWebViewBuilder]. - static Map creationParamsToMap( - CreationParams creationParams) { - return { - 'initialUrl': creationParams.initialUrl, - 'settings': _webSettingsToMap(creationParams.webSettings), - 'javascriptChannelNames': creationParams.javascriptChannelNames?.toList(), - 'userAgent': creationParams.userAgent, - 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, - }; - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/lib/webview_flutter.dart b/apps/flutter_parent/plugins/webview_flutter/lib/webview_flutter.dart deleted file mode 100644 index 3a846f6c22..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/lib/webview_flutter.dart +++ /dev/null @@ -1,823 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import 'platform_interface.dart'; -import 'src/webview_android.dart'; -import 'src/webview_cupertino.dart'; -import 'src/webview_method_channel.dart'; - -/// Optional callback invoked when a web view is first created. [controller] is -/// the [WebViewController] for the created web view. -typedef void WebViewCreatedCallback(WebViewController controller); - -/// Describes the state of JavaScript support in a given web view. -enum JavascriptMode { - /// JavaScript execution is disabled. - disabled, - - /// JavaScript execution is not restricted. - unrestricted, -} - -/// A message that was sent by JavaScript code running in a [WebView]. -class JavascriptMessage { - /// Constructs a JavaScript message object. - /// - /// The `message` parameter must not be null. - const JavascriptMessage(this.message); - - /// The contents of the message that was sent by the JavaScript code. - final String message; -} - -/// Callback type for handling messages sent from Javascript running in a web view. -typedef void JavascriptMessageHandler(JavascriptMessage message); - -/// Information about a navigation action that is about to be executed. -class NavigationRequest { - NavigationRequest._({ required this.url, required this.isForMainFrame}); - - /// The URL that will be loaded if the navigation is executed. - final String url; - - /// Whether the navigation request is to be loaded as the main frame. - final bool isForMainFrame; - - @override - String toString() { - return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; - } -} - -/// A decision on how to handle a navigation request. -enum NavigationDecision { - /// Prevent the navigation from taking place. - prevent, - - /// Allow the navigation to take place. - navigate, -} - -/// Android [WebViewPlatform] that uses [AndroidViewSurface] to build the [WebView] widget. -/// -/// To use this, set [WebView.platform] to an instance of this class. -/// -/// This implementation uses hybrid composition to render the [WebView] on -/// Android. It solves multiple issues related to accessibility and interaction -/// with the [WebView] at the cost of some performance on Android versions below -/// 10. See https://github.com/flutter/flutter/wiki/Hybrid-Composition for more -/// information. -class SurfaceAndroidWebView extends AndroidWebView { - @override - Widget build({ - BuildContext? context, - CreationParams? creationParams, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, - }) { - assert(webViewPlatformCallbacksHandler != null); - return PlatformViewLink( - viewType: 'plugins.flutter.io/webview', - surfaceFactory: ( - BuildContext context, - PlatformViewController controller, - ) { - return AndroidViewSurface( - controller: controller as AndroidViewController, - gestureRecognizers: gestureRecognizers ?? - const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - onCreatePlatformView: (PlatformViewCreationParams params) { - return PlatformViewsService.initSurfaceAndroidView( - id: params.id, - viewType: 'plugins.flutter.io/webview', - // WebView content is not affected by the Android view's layout direction, - // we explicitly set it here so that the widget doesn't require an ambient - // directionality. - layoutDirection: TextDirection.rtl, - creationParams: creationParams != null ? MethodChannelWebViewPlatform.creationParamsToMap(creationParams) : null, - creationParamsCodec: const StandardMessageCodec(), - ) - ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) - ..addOnPlatformViewCreatedListener((int id) { - if (onWebViewPlatformCreated == null || webViewPlatformCallbacksHandler == null) { - return; - } - onWebViewPlatformCreated( - MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler), - ); - }) - ..create(); - }, - ); - } -} - -/// Decides how to handle a specific navigation request. -/// -/// The returned [NavigationDecision] determines how the navigation described by -/// `navigation` should be handled. -/// -/// See also: [WebView.navigationDelegate]. -typedef FutureOr NavigationDelegate( - NavigationRequest navigation); - -/// Signature for when a [WebView] has started loading a page. -typedef void PageStartedCallback(String url); - -/// Signature for when a [WebView] has finished loading a page. -typedef void PageFinishedCallback(String url); - -/// Signature for when a [WebView] has failed to load a resource. -typedef void WebResourceErrorCallback(WebResourceError error); - -/// Specifies possible restrictions on automatic media playback. -/// -/// This is typically used in [WebView.initialMediaPlaybackPolicy]. -// The method channel implementation is marshalling this enum to the value's index, so the order -// is important. -enum AutoMediaPlaybackPolicy { - /// Starting any kind of media playback requires a user action. - /// - /// For example: JavaScript code cannot start playing media unless the code was executed - /// as a result of a user action (like a touch event). - require_user_action_for_all_media_types, - - /// Starting any kind of media playback is always allowed. - /// - /// For example: JavaScript code that's triggered when the page is loaded can start playing - /// video or audio. - always_allow, -} - -final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9_]*\$'); - -/// A named channel for receiving messaged from JavaScript code running inside a web view. -class JavascriptChannel { - /// Constructs a Javascript channel. - /// - /// The parameters `name` and `onMessageReceived` must not be null. - JavascriptChannel({ - required this.name, - required this.onMessageReceived, - }) : assert(_validChannelNames.hasMatch(name)); - - /// The channel's name. - /// - /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to - /// the Javascript window object's property named `name`. - /// - /// The name must start with a letter or underscore(_), followed by any combination of those - /// characters plus digits. - /// - /// Note that any JavaScript existing `window` property with this name will be overriden. - /// - /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism. - final String name; - - /// A callback that's invoked when a message is received through the channel. - final JavascriptMessageHandler onMessageReceived; -} - -/// A web view widget for showing html content. -/// -/// There is a known issue that on iOS 13.4 and 13.5, other flutter widgets covering -/// the `WebView` is not able to block the `WebView` from receiving touch events. -/// See https://github.com/flutter/flutter/issues/53490. -class WebView extends StatefulWidget { - /// Creates a new web view. - /// - /// The web view can be controlled using a `WebViewController` that is passed to the - /// `onWebViewCreated` callback once the web view is created. - /// - /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. - const WebView({ - Key? key, - this.onWebViewCreated, - this.initialUrl, - this.javascriptMode = JavascriptMode.disabled, - this.javascriptChannels, - this.navigationDelegate, - this.gestureRecognizers, - this.onPageStarted, - this.onPageFinished, - this.onWebResourceError, - this.darkMode = false, - this.debuggingEnabled = false, - this.gestureNavigationEnabled = false, - this.userAgent, - this.initialMediaPlaybackPolicy = - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - }) : super(key: key); - - static WebViewPlatform? _platform; - - /// Sets a custom [WebViewPlatform]. - /// - /// This property can be set to use a custom platform implementation for WebViews. - /// - /// Setting `platform` doesn't affect [WebView]s that were already created. - /// - /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. - static set platform(WebViewPlatform platform) { - _platform = platform; - } - - /// The WebView platform that's used by this WebView. - /// - /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. - static WebViewPlatform get platform { - if (_platform == null) { - switch (defaultTargetPlatform) { - case TargetPlatform.android: - _platform = AndroidWebView(); - break; - case TargetPlatform.iOS: - _platform = CupertinoWebView(); - break; - default: - throw UnsupportedError( - "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); - } - } - return _platform!; - } - - /// If not null invoked once the web view is created. - final WebViewCreatedCallback? onWebViewCreated; - - /// Which gestures should be consumed by the web view. - /// - /// It is possible for other gesture recognizers to be competing with the web view on pointer - /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle - /// vertical drags. The web view will claim gestures that are recognized by any of the - /// recognizers on this list. - /// - /// When this set is empty or null, the web view will only handle pointer events for gestures that - /// were not claimed by any other gesture recognizer. - final Set>? gestureRecognizers; - - /// The initial URL to load. - final String? initialUrl; - - /// Whether Javascript execution is enabled. - final JavascriptMode javascriptMode; - - /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. - /// - /// For each [JavascriptChannel] in the set, a channel object is made available for the - /// JavaScript code in a window property named [JavascriptChannel.name]. - /// The JavaScript code can then call `postMessage` on that object to send a message that will be - /// passed to [JavascriptChannel.onMessageReceived]. - /// - /// For example for the following JavascriptChannel: - /// - /// ```dart - /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); - /// ``` - /// - /// JavaScript code can call: - /// - /// ```javascript - /// Print.postMessage('Hello'); - /// ``` - /// - /// To asynchronously invoke the message handler which will print the message to standard output. - /// - /// Adding a new JavaScript channel only takes affect after the next page is loaded. - /// - /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple - /// channels in the list. - /// - /// A null value is equivalent to an empty set. - final Set? javascriptChannels; - - /// A delegate function that decides how to handle navigation actions. - /// - /// When a navigation is initiated by the WebView (e.g when a user clicks a link) - /// this delegate is called and has to decide how to proceed with the navigation. - /// - /// See [NavigationDecision] for possible decisions the delegate can take. - /// - /// When null all navigation actions are allowed. - /// - /// Caveats on Android: - /// - /// * Navigation actions targeted to the main frame can be intercepted, - /// navigation actions targeted to subframes are allowed regardless of the value - /// returned by this delegate. - /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were - /// triggered by a user gesture, this disables some of Chromium's security mechanisms. - /// A navigationDelegate should only be set when loading trusted content. - /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have - /// a later version): - /// * When a navigationDelegate is set pages with frames are not properly handled by the - /// webview, and frames will be opened in the main frame. - /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. - final NavigationDelegate? navigationDelegate; - - /// Invoked when a page starts loading. - final PageStartedCallback? onPageStarted; - - /// Invoked when a page has finished loading. - /// - /// This is invoked only for the main frame. - /// - /// When [onPageFinished] is invoked on Android, the page being rendered may - /// not be updated yet. - /// - /// When invoked on iOS or Android, any Javascript code that is embedded - /// directly in the HTML has been loaded and code injected with - /// [WebViewController.evaluateJavascript] can assume this. - final PageFinishedCallback? onPageFinished; - - /// Invoked when a web resource has failed to load. - /// - /// This can be called for any resource (iframe, image, etc.), not just for - /// the main page. - final WebResourceErrorCallback? onWebResourceError; - - final bool? darkMode; - - /// Controls whether WebView debugging is enabled. - /// - /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). - /// - /// WebView debugging is enabled by default in dev builds on iOS. - /// - /// To debug WebViews on iOS: - /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) - /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> - /// - /// By default `debuggingEnabled` is false. - final bool debuggingEnabled; - - /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. - /// - /// This only works on iOS. - /// - /// By default `gestureNavigationEnabled` is false. - final bool gestureNavigationEnabled; - - /// The value used for the HTTP User-Agent: request header. - /// - /// When null the platform's webview default is used for the User-Agent header. - /// - /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. - /// - /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. - /// - /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom - /// user agent. - /// - /// By default `userAgent` is null. - final String? userAgent; - - /// Which restrictions apply on automatic media playback. - /// - /// This initial value is applied to the platform's webview upon creation. Any following - /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). - /// - /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. - final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; - - @override - State createState() => _WebViewState(); -} - -class _WebViewState extends State { - final Completer _controller = - Completer(); - - late _PlatformCallbacksHandler _platformCallbacksHandler; - - @override - Widget build(BuildContext context) { - return WebView.platform.build( - context: context, - onWebViewPlatformCreated: _onWebViewPlatformCreated, - webViewPlatformCallbacksHandler: _platformCallbacksHandler, - gestureRecognizers: widget.gestureRecognizers, - creationParams: _creationParamsfromWidget(widget), - ); - } - - @override - void initState() { - super.initState(); - _assertJavascriptChannelNamesAreUnique(); - _platformCallbacksHandler = _PlatformCallbacksHandler(widget); - } - - @override - void didUpdateWidget(WebView oldWidget) { - super.didUpdateWidget(oldWidget); - _assertJavascriptChannelNamesAreUnique(); - _controller.future.then((WebViewController controller) { - _platformCallbacksHandler._widget = widget; - controller._updateWidget(widget); - }); - } - - void _onWebViewPlatformCreated(WebViewPlatformController webViewPlatform) { - final WebViewController controller = - WebViewController._(widget, webViewPlatform, _platformCallbacksHandler); - _controller.complete(controller); - if (widget.onWebViewCreated != null) { - widget.onWebViewCreated!(controller); - } - } - - void _assertJavascriptChannelNamesAreUnique() { - if (widget.javascriptChannels == null || - widget.javascriptChannels!.isEmpty) { - return; - } - assert(_extractChannelNames(widget.javascriptChannels)?.length == - widget.javascriptChannels!.length); - } -} - -CreationParams _creationParamsfromWidget(WebView widget) { - return CreationParams( - initialUrl: widget.initialUrl, - webSettings: _webSettingsFromWidget(widget), - javascriptChannelNames: _extractChannelNames(widget.javascriptChannels), - userAgent: widget.userAgent, - autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, - ); -} - -WebSettings _webSettingsFromWidget(WebView widget) { - return WebSettings( - javascriptMode: widget.javascriptMode, - hasNavigationDelegate: widget.navigationDelegate != null, - darkMode: widget.darkMode, - debuggingEnabled: widget.debuggingEnabled, - gestureNavigationEnabled: widget.gestureNavigationEnabled, - userAgent: WebSetting.of(widget.userAgent ?? ''), - ); -} - -// This method assumes that no fields in `currentValue` are null. -WebSettings _clearUnchangedWebSettings( - WebSettings currentValue, WebSettings newValue) { - assert(currentValue.javascriptMode != null); - assert(currentValue.hasNavigationDelegate != null); - assert(currentValue.darkMode != null); - assert(currentValue.debuggingEnabled != null); - assert(currentValue.userAgent.isPresent); - assert(newValue.javascriptMode != null); - assert(newValue.hasNavigationDelegate != null); - assert(newValue.darkMode != null); - assert(newValue.debuggingEnabled != null); - assert(newValue.userAgent.isPresent); - - JavascriptMode? javascriptMode; - bool? hasNavigationDelegate; - bool? darkMode; - bool? debuggingEnabled; - WebSetting userAgent = WebSetting.absent(); - if (currentValue.javascriptMode != newValue.javascriptMode) { - javascriptMode = newValue.javascriptMode; - } - if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { - hasNavigationDelegate = newValue.hasNavigationDelegate; - } - if (currentValue.darkMode != newValue.darkMode) { - darkMode = newValue.darkMode; - } - if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { - debuggingEnabled = newValue.debuggingEnabled; - } - if (currentValue.userAgent != newValue.userAgent) { - userAgent = newValue.userAgent; - } - - return WebSettings( - javascriptMode: javascriptMode, - hasNavigationDelegate: hasNavigationDelegate, - darkMode: darkMode, - debuggingEnabled: debuggingEnabled, - userAgent: userAgent, - ); -} - -Set? _extractChannelNames(Set? channels) { - final Set channelNames = channels == null - ? {} - : channels.map((JavascriptChannel channel) => channel.name).toSet(); - return channelNames; -} - -class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { - _PlatformCallbacksHandler(this._widget) { - _updateJavascriptChannelsFromSet(_widget.javascriptChannels); - } - - WebView _widget; - - // Maps a channel name to a channel. - final Map _javascriptChannels = - {}; - - @override - void onJavaScriptChannelMessage(String channel, String message) { - _javascriptChannels[channel]?.onMessageReceived(JavascriptMessage(message)); - } - - @override - FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) async { - final NavigationRequest request = - NavigationRequest._(url: url!, isForMainFrame: isForMainFrame!); - final bool allowNavigation = _widget.navigationDelegate == null || - await _widget.navigationDelegate!(request) == - NavigationDecision.navigate; - return allowNavigation; - } - - @override - void onPageStarted(String url) { - if (_widget.onPageStarted != null) { - _widget.onPageStarted!(url); - } - } - - @override - void onPageFinished(String url) { - if (_widget.onPageFinished != null) { - _widget.onPageFinished!(url); - } - } - - @override - void onWebResourceError(WebResourceError error) { - if (_widget.onWebResourceError != null) { - _widget.onWebResourceError!(error); - } - } - - void _updateJavascriptChannelsFromSet(Set? channels) { - _javascriptChannels.clear(); - if (channels == null) { - return; - } - for (JavascriptChannel channel in channels) { - _javascriptChannels[channel.name] = channel; - } - } -} - -/// Controls a [WebView]. -/// -/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] -/// callback for a [WebView] widget. -class WebViewController { - WebViewController._( - this._widget, - this._webViewPlatformController, - this._platformCallbacksHandler, - ) { - _settings = _webSettingsFromWidget(_widget); - } - - final WebViewPlatformController _webViewPlatformController; - - final _PlatformCallbacksHandler _platformCallbacksHandler; - - late WebSettings _settings; - - WebView _widget; - - /// Loads the specified URL. - /// - /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will - /// be added as key value pairs of HTTP headers for the request. - /// - /// `url` must not be null. - /// - /// Throws an ArgumentError if `url` is not a valid URL string. - Future loadUrl( - String url, { - Map? headers, - }) async { - _validateUrlString(url); - return _webViewPlatformController.loadUrl(url, headers); - } - - Future loadData( - String? baseUrl, - String data, - String mimeType, - String encoding) async { - return _webViewPlatformController.loadData(baseUrl, data, mimeType, encoding); - } - - Future setAcceptThirdPartyCookies(bool accept) async { - return _webViewPlatformController.setAcceptThirdPartyCookies(accept); - } - - Future setCookie(String url, String value) async { - return _webViewPlatformController.setCookie(url, value); - } - - /// Accessor to the current URL that the WebView is displaying. - /// - /// If [WebView.initialUrl] was never specified, returns `null`. - /// Note that this operation is asynchronous, and it is possible that the - /// current URL changes again by the time this function returns (in other - /// words, by the time this future completes, the WebView may be displaying a - /// different URL). - Future currentUrl() { - return _webViewPlatformController.currentUrl(); - } - - /// Checks whether there's a back history item. - /// - /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has - /// changed by the time the future completed. - Future canGoBack() { - return _webViewPlatformController.canGoBack(); - } - - /// Checks whether there's a forward history item. - /// - /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has - /// changed by the time the future completed. - Future canGoForward() { - return _webViewPlatformController.canGoForward(); - } - - /// Goes back in the history of this WebView. - /// - /// If there is no back history item this is a no-op. - Future goBack() { - return _webViewPlatformController.goBack(); - } - - /// Goes forward in the history of this WebView. - /// - /// If there is no forward history item this is a no-op. - Future goForward() { - return _webViewPlatformController.goForward(); - } - - /// Reloads the current URL. - Future reload() { - return _webViewPlatformController.reload(); - } - - /// Clears all caches used by the [WebView]. - /// - /// The following caches are cleared: - /// 1. Browser HTTP Cache. - /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. - /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. - /// 3. Application cache. - /// 4. Local Storage. - /// - /// Note: Calling this method also triggers a reload. - Future clearCache() async { - await _webViewPlatformController.clearCache(); - return reload(); - } - - Future _updateWidget(WebView widget) async { - _widget = widget; - await _updateSettings(_webSettingsFromWidget(widget)); - await _updateJavascriptChannels(widget.javascriptChannels); - } - - Future _updateSettings(WebSettings newSettings) { - final WebSettings update = - _clearUnchangedWebSettings(_settings, newSettings); - _settings = newSettings; - return _webViewPlatformController.updateSettings(update); - } - - Future _updateJavascriptChannels( - Set? newChannels) async { - final Set currentChannels = - _platformCallbacksHandler._javascriptChannels.keys.toSet(); - final Set newChannelNames = _extractChannelNames(newChannels)!; - final Set channelsToAdd = - newChannelNames.difference(currentChannels); - final Set channelsToRemove = - currentChannels.difference(newChannelNames); - if (channelsToRemove.isNotEmpty) { - await _webViewPlatformController - .removeJavascriptChannels(channelsToRemove); - } - if (channelsToAdd.isNotEmpty) { - await _webViewPlatformController.addJavascriptChannels(channelsToAdd); - } - _platformCallbacksHandler._updateJavascriptChannelsFromSet(newChannels); - } - - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// On Android returns the evaluation result as a JSON formatted string. - /// - /// On iOS depending on the value type the return value would be one of: - /// - /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). - /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). - /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. - /// - /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the - /// evaluated expression is not supported as described above. - /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript - /// embedded in the main frame HTML has been loaded. - Future evaluateJavascript(String? javascriptString) { - if (_settings.javascriptMode == JavascriptMode.disabled) { - return Future.error(FlutterError( - 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); - } - if (javascriptString == null) { - return Future.error( - ArgumentError('The argument javascriptString must not be null.')); - } - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _webViewPlatformController.evaluateJavascript(javascriptString); - } - - /// Returns the title of the currently loaded page. - Future getTitle() { - return _webViewPlatformController.getTitle(); - } - - /// Sets the WebView's content scroll position. - /// - /// The parameters `x` and `y` specify the scroll position in WebView pixels. - Future scrollTo(int x, int y) { - return _webViewPlatformController.scrollTo(x, y); - } - - /// Move the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. - Future scrollBy(int x, int y) { - return _webViewPlatformController.scrollBy(x, y); - } - - /// Return the horizontal scroll position, in WebView pixels, of this view. - /// - /// Scroll position is measured from left. - Future getScrollX() { - return _webViewPlatformController.getScrollX(); - } - - /// Return the vertical scroll position, in WebView pixels, of this view. - /// - /// Scroll position is measured from top. - Future getScrollY() { - return _webViewPlatformController.getScrollY(); - } -} - -/// Manages cookies pertaining to all [WebView]s. -class CookieManager { - /// Creates a [CookieManager] -- returns the instance if it's already been called. - factory CookieManager() { - return _instance ??= CookieManager._(); - } - - CookieManager._(); - - static CookieManager? _instance; - - /// Clears all cookies for all [WebView] instances. - /// - /// This is a no op on iOS version smaller than 9. - /// - /// Returns true if cookies were present before clearing, else false. - Future clearCookies() => WebView.platform.clearCookies(); -} - -// Throws an ArgumentError if `url` is not a valid URL string. -void _validateUrlString(String url) { - try { - final Uri uri = Uri.parse(url); - if (uri.scheme.isEmpty) { - throw ArgumentError('Missing scheme in URL string: "$url"'); - } - } on FormatException catch (e) { - throw ArgumentError(e); - } -} diff --git a/apps/flutter_parent/plugins/webview_flutter/pubspec.lock b/apps/flutter_parent/plugins/webview_flutter/pubspec.lock deleted file mode 100644 index e30d186c42..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/pubspec.lock +++ /dev/null @@ -1,220 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - archive: - dependency: transitive - description: - name: archive - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.2" - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - file: - dependency: transitive - description: - name: file - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.2" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_driver: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - fuchsia_remote_debug_protocol: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.10" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - pedantic: - dependency: "direct dev" - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0+1" - platform: - dependency: transitive - description: - name: platform - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - process: - dependency: transitive - description: - name: process - url: "https://pub.dartlang.org" - source: hosted - version: "4.2.3" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - sync_http: - dependency: transitive - description: - name: sync_http - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.2" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - vm_service: - dependency: transitive - description: - name: vm_service - url: "https://pub.dartlang.org" - source: hosted - version: "7.1.1" - webdriver: - dependency: transitive - description: - name: webdriver - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" -sdks: - dart: ">=2.12.0 <3.0.0" - flutter: ">=2.5.3" diff --git a/apps/flutter_parent/plugins/webview_flutter/pubspec.yaml b/apps/flutter_parent/plugins/webview_flutter/pubspec.yaml deleted file mode 100644 index 60fc18d241..0000000000 --- a/apps/flutter_parent/plugins/webview_flutter/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: webview_flutter -description: A Flutter plugin that provides a WebView widget on Android and iOS. -version: 1.0.7 -homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter -publish_to: none - -environment: - sdk: ">=3.0.0 <3.10.6" - flutter: 3.13.2 - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - pedantic: ^1.8.0 - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.webviewflutter - pluginClass: WebViewFlutterPlugin - ios: - pluginClass: FLTWebViewFlutterPlugin diff --git a/apps/flutter_parent/pubspec.lock b/apps/flutter_parent/pubspec.lock index bd0a5ac6b2..f05ba9414b 100644 --- a/apps/flutter_parent/pubspec.lock +++ b/apps/flutter_parent/pubspec.lock @@ -1518,10 +1518,35 @@ packages: webview_flutter: dependency: "direct main" description: - path: "plugins/webview_flutter" - relative: true - source: path - version: "1.0.7" + name: webview_flutter + sha256: "392c1d83b70fe2495de3ea2c84531268d5b8de2de3f01086a53334d8b6030a88" + url: "https://pub.dev" + source: hosted + version: "3.0.4" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "8b3b2450e98876c70bfcead876d9390573b34b9418c19e28168b74f6cb252dbd" + url: "https://pub.dev" + source: hosted + version: "2.10.4" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "812165e4e34ca677bdfbfa58c01e33b27fd03ab5fa75b70832d4b7d4ca1fa8cf" + url: "https://pub.dev" + source: hosted + version: "1.9.5" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: a5364369c758892aa487cbf59ea41d9edd10f9d9baf06a94e80f1bd1b4c7bbc0 + url: "https://pub.dev" + source: hosted + version: "2.9.5" win32: dependency: transitive description: diff --git a/apps/flutter_parent/pubspec.yaml b/apps/flutter_parent/pubspec.yaml index 9f2afddcc1..013bf5be43 100644 --- a/apps/flutter_parent/pubspec.yaml +++ b/apps/flutter_parent/pubspec.yaml @@ -25,7 +25,7 @@ description: Canvas Parent # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 3.9.0+48 +version: 3.9.1+49 module: androidX: true @@ -89,9 +89,7 @@ dependencies: package_info_plus: ^4.0.2 permission_handler: ^10.4.3 shared_preferences: ^2.2.0 # Used to cache remote config properties - #webview_flutter: 0.3.19+5 - webview_flutter: # TODO: Remove once the flutter plugin supports baseUrl https://github.com/flutter/plugins/pull/2463 - path: ./plugins/webview_flutter + webview_flutter: ^3.0.4 # Routing fluro: ^2.0.5 diff --git a/apps/flutter_parent/test/utils/test_app.dart b/apps/flutter_parent/test/utils/test_app.dart index f477beeab6..62d116d4b4 100644 --- a/apps/flutter_parent/test/utils/test_app.dart +++ b/apps/flutter_parent/test/utils/test_app.dart @@ -12,8 +12,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'dart:ui'; - import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -30,12 +28,13 @@ import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_parent/utils/design/theme_prefs.dart'; import 'package:flutter_parent/utils/remote_config_utils.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_test/src/deprecated.dart'; import 'package:get_it/get_it.dart'; +import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'platform_config.dart'; -import 'test_helpers/mock_helpers.dart'; import 'test_helpers/mock_helpers.mocks.dart'; class TestApp extends StatefulWidget { @@ -259,28 +258,33 @@ Future setupPlatformChannels({PlatformConfig config = const PlatformConfig } } -/// WebView helpers. These are needed as web views tie into platform views. These are special though as the channel -/// name depends on the platform view's ID. This makes mocking these generically difficult as each id has a different -/// platform channel to register. +/// WebView helpers. These are needed as web views tie into platform views. /// /// Inspired solution is a slimmed down version of the WebView test: -/// https://github.com/flutter/plugins/blob/3b71d6e9a4456505f0b079074fcbc9ba9f8e0e15/packages/webview_flutter/test/webview_flutter_test.dart +/// https://github.com/flutter/plugins/blob/webview_flutter-v3.0.4/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart void _initPlatformWebView() { - const MethodChannel('plugins.flutter.io/cookie_manager', const StandardMethodCodec()) - .setMockMethodCallHandler((_) => Future.sync(() => false)); - - // Intercept when a web view is getting created so we can set up the platform channel - SystemChannels.platform_views.setMockMethodCallHandler((call) { - switch (call.method) { - case 'create': - final id = call.arguments['id']; - MethodChannel('plugins.flutter.io/webview_$id', const StandardMethodCodec()) - .setMockMethodCallHandler((_) => Future.sync(() {})); - return Future.sync(() => 1); - default: - return Future.sync(() {}); - } + final mockWebViewPlatformController = MockWebViewPlatformController(); + final mockWebViewPlatform = MockWebViewPlatform(); + when(mockWebViewPlatform.build( + context: anyNamed('context'), + creationParams: anyNamed('creationParams'), + webViewPlatformCallbacksHandler: + anyNamed('webViewPlatformCallbacksHandler'), + javascriptChannelRegistry: anyNamed('javascriptChannelRegistry'), + onWebViewPlatformCreated: anyNamed('onWebViewPlatformCreated'), + gestureRecognizers: anyNamed('gestureRecognizers'), + )).thenAnswer((Invocation invocation) { + final WebViewPlatformCreatedCallback onWebViewPlatformCreated = + invocation.namedArguments[const Symbol('onWebViewPlatformCreated')] + as WebViewPlatformCreatedCallback; + return TestPlatformWebView( + mockWebViewPlatformController: mockWebViewPlatformController, + onWebViewPlatformCreated: onWebViewPlatformCreated, + ); }); + + WebView.platform = mockWebViewPlatform; + WebViewCookieManagerPlatform.instance = FakeWebViewCookieManager(); } /// Mocks the platform channel used by the package_info plugin @@ -391,4 +395,47 @@ void _initPathProvider() { } return null; }); +} + +class TestPlatformWebView extends StatefulWidget { + const TestPlatformWebView({ + Key? key, + required this.mockWebViewPlatformController, + this.onWebViewPlatformCreated, + }) : super(key: key); + + final MockWebViewPlatformController mockWebViewPlatformController; + final WebViewPlatformCreatedCallback? onWebViewPlatformCreated; + + @override + State createState() => TestPlatformWebViewState(); +} + +class TestPlatformWebViewState extends State { + @override + void initState() { + super.initState(); + final WebViewPlatformCreatedCallback? onWebViewPlatformCreated = + widget.onWebViewPlatformCreated; + if (onWebViewPlatformCreated != null) { + onWebViewPlatformCreated(widget.mockWebViewPlatformController); + } + } + + @override + Widget build(BuildContext context) { + return Container(); + } +} + +class FakeWebViewCookieManager extends WebViewCookieManagerPlatform { + @override + Future clearCookies() { + return Future.value(false); + } + + @override + Future setCookie(WebViewCookie cookie) { + return Future.value(null); + } } \ No newline at end of file diff --git a/apps/flutter_parent/test/utils/test_helpers/mock_helpers.dart b/apps/flutter_parent/test/utils/test_helpers/mock_helpers.dart index 21f738eccc..a7b3985f1e 100644 --- a/apps/flutter_parent/test/utils/test_helpers/mock_helpers.dart +++ b/apps/flutter_parent/test/utils/test_helpers/mock_helpers.dart @@ -19,9 +19,7 @@ // settings will correspond the specified values. import 'dart:io'; -import 'package:barcode_scan2/platform_wrapper.dart'; import 'package:dio/dio.dart'; -import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/material.dart'; @@ -45,7 +43,6 @@ import 'package:flutter_parent/network/api/planner_api.dart'; import 'package:flutter_parent/network/api/user_api.dart'; import 'package:flutter_parent/network/utils/analytics.dart'; import 'package:flutter_parent/network/utils/authentication_interceptor.dart'; -import 'package:flutter_parent/network/utils/dio_config.dart'; import 'package:flutter_parent/screens/account_creation/account_creation_interactor.dart'; import 'package:flutter_parent/screens/alert_thresholds/alert_thresholds_interactor.dart'; import 'package:flutter_parent/screens/alerts/alerts_interactor.dart'; @@ -60,7 +57,6 @@ import 'package:flutter_parent/screens/courses/routing_shell/course_routing_shel import 'package:flutter_parent/screens/dashboard/alert_notifier.dart'; import 'package:flutter_parent/screens/dashboard/dashboard_interactor.dart'; import 'package:flutter_parent/screens/dashboard/inbox_notifier.dart'; -import 'package:flutter_parent/screens/dashboard/selected_student_notifier.dart'; import 'package:flutter_parent/screens/domain_search/domain_search_interactor.dart'; import 'package:flutter_parent/screens/events/event_details_interactor.dart'; import 'package:flutter_parent/screens/help/help_screen_interactor.dart'; @@ -102,6 +98,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:sqflite/sqflite.dart'; import 'package:video_player/video_player.dart'; +import 'package:webview_flutter/webview_flutter.dart'; @GenerateNiceMocks([ MockSpec(), @@ -189,6 +186,8 @@ import 'package:video_player/video_player.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec() ]) import 'mock_helpers.mocks.dart'; diff --git a/apps/flutter_parent/test/utils/test_helpers/mock_helpers.mocks.dart b/apps/flutter_parent/test/utils/test_helpers/mock_helpers.mocks.dart index aeb97f61de..6b15d639d3 100644 --- a/apps/flutter_parent/test/utils/test_helpers/mock_helpers.mocks.dart +++ b/apps/flutter_parent/test/utils/test_helpers/mock_helpers.mocks.dart @@ -20,6 +20,7 @@ import 'package:firebase_remote_config_platform_interface/firebase_remote_config as _i14; import 'package:fluro/fluro.dart' as _i84; import 'package:flutter/foundation.dart' as _i11; +import 'package:flutter/gestures.dart' as _i142; import 'package:flutter/material.dart' as _i17; import 'package:flutter/services.dart' as _i15; import 'package:flutter_downloader/flutter_downloader.dart' as _i133; @@ -9196,3 +9197,319 @@ class MockRemoteConfigInteractor extends _i1.Mock returnValueForMissingStub: null, ); } + +/// A class which mocks [WebViewPlatformController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatformController extends _i1.Mock + implements _i16.WebViewPlatformController { + @override + _i8.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( + Invocation.method( + #loadFile, + [absoluteFilePath], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method( + #loadFlutterAsset, + [key], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future loadHtmlString( + String? html, { + String? baseUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadHtmlString, + [html], + {#baseUrl: baseUrl}, + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future loadUrl( + String? url, + Map? headers, + ) => + (super.noSuchMethod( + Invocation.method( + #loadUrl, + [ + url, + headers, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future loadRequest(_i16.WebViewRequest? request) => + (super.noSuchMethod( + Invocation.method( + #loadRequest, + [request], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future updateSettings(_i16.WebSettings? setting) => + (super.noSuchMethod( + Invocation.method( + #updateSettings, + [setting], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future currentUrl() => (super.noSuchMethod( + Invocation.method( + #currentUrl, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); + @override + _i8.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future clearCache() => (super.noSuchMethod( + Invocation.method( + #clearCache, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future evaluateJavascript(String? javascript) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavascript, + [javascript], + ), + returnValue: _i8.Future.value(''), + returnValueForMissingStub: _i8.Future.value(''), + ) as _i8.Future); + @override + _i8.Future runJavascript(String? javascript) => (super.noSuchMethod( + Invocation.method( + #runJavascript, + [javascript], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future runJavascriptReturningResult(String? javascript) => + (super.noSuchMethod( + Invocation.method( + #runJavascriptReturningResult, + [javascript], + ), + returnValue: _i8.Future.value(''), + returnValueForMissingStub: _i8.Future.value(''), + ) as _i8.Future); + @override + _i8.Future addJavascriptChannels(Set? javascriptChannelNames) => + (super.noSuchMethod( + Invocation.method( + #addJavascriptChannels, + [javascriptChannelNames], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future removeJavascriptChannels( + Set? javascriptChannelNames) => + (super.noSuchMethod( + Invocation.method( + #removeJavascriptChannels, + [javascriptChannelNames], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future scrollTo( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + x, + y, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future scrollBy( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + x, + y, + ], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future getScrollX() => (super.noSuchMethod( + Invocation.method( + #getScrollX, + [], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + _i8.Future getScrollY() => (super.noSuchMethod( + Invocation.method( + #getScrollY, + [], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); +} + +/// A class which mocks [WebViewPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatform extends _i1.Mock implements _i16.WebViewPlatform { + @override + _i17.Widget build({ + required _i17.BuildContext? context, + required _i16.CreationParams? creationParams, + required _i16.WebViewPlatformCallbacksHandler? + webViewPlatformCallbacksHandler, + required _i16.JavascriptChannelRegistry? javascriptChannelRegistry, + _i16.WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set<_i11.Factory<_i142.OneSequenceGestureRecognizer>>? gestureRecognizers, + }) => + (super.noSuchMethod( + Invocation.method( + #build, + [], + { + #context: context, + #creationParams: creationParams, + #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + #javascriptChannelRegistry: javascriptChannelRegistry, + #onWebViewPlatformCreated: onWebViewPlatformCreated, + #gestureRecognizers: gestureRecognizers, + }, + ), + returnValue: _FakeWidget_30( + this, + Invocation.method( + #build, + [], + { + #context: context, + #creationParams: creationParams, + #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + #javascriptChannelRegistry: javascriptChannelRegistry, + #onWebViewPlatformCreated: onWebViewPlatformCreated, + #gestureRecognizers: gestureRecognizers, + }, + ), + ), + returnValueForMissingStub: _FakeWidget_30( + this, + Invocation.method( + #build, + [], + { + #context: context, + #creationParams: creationParams, + #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + #javascriptChannelRegistry: javascriptChannelRegistry, + #onWebViewPlatformCreated: onWebViewPlatformCreated, + #gestureRecognizers: gestureRecognizers, + }, + ), + ), + ) as _i17.Widget); + @override + _i8.Future clearCookies() => (super.noSuchMethod( + Invocation.method( + #clearCookies, + [], + ), + returnValue: _i8.Future.value(false), + returnValueForMissingStub: _i8.Future.value(false), + ) as _i8.Future); +} diff --git a/apps/student/build.gradle b/apps/student/build.gradle index df8c0ecaa2..a77df72903 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -320,7 +320,8 @@ dependencies { implementation Libs.SQLDELIGHT /* Qr Code */ - implementation Libs.JOURNEY_ZXING + implementation (Libs.JOURNEY_ZXING) { transitive = false } + implementation Libs.JOURNEY_ZXING_CORE /* AAC */ implementation Libs.VIEW_MODEL @@ -349,6 +350,8 @@ dependencies { implementation Libs.ROOM kapt Libs.ROOM_COMPILER implementation Libs.ROOM_COROUTINES + + testImplementation Libs.HAMCREST } // Comment out this line if the reporting logic starts going wonky. diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt index b2d370238f..0f5b709514 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt @@ -25,6 +25,8 @@ import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi import com.instructure.dataseeding.model.CanvasUserApiModel +import com.instructure.espresso.retry +import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -113,6 +115,7 @@ class InboxE2ETest: StudentTest() { inboxPage.selectConversation(seededConversation) inboxPage.assertSelectedConversationNumber("1") inboxPage.clickUnArchive() + inboxPage.assertInboxEmpty() inboxPage.assertConversationNotDisplayed(seededConversation.subject) @@ -127,7 +130,10 @@ class InboxE2ETest: StudentTest() { inboxPage.selectConversations(listOf(seededConversation.subject)) inboxPage.assertSelectedConversationNumber("1") inboxPage.clickUnstar() - inboxPage.assertConversationNotStarred(seededConversation.subject) + + retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) { + inboxPage.assertConversationNotStarred(seededConversation.subject) + } Log.d(STEP_TAG, "Select the conversations (${seededConversation.subject} and archive it. Assert that it has not displayed in the 'INBOX' scope.") inboxPage.selectConversations(listOf(seededConversation.subject)) @@ -161,7 +167,10 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG, "Select the conversation. Unarchive it, and assert that it has not displayed in the 'ARCHIVED' scope.") inboxPage.selectConversations(listOf(seededConversation.subject)) inboxPage.clickUnArchive() - inboxPage.assertConversationNotDisplayed(seededConversation.subject) + + retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) { + inboxPage.assertConversationNotDisplayed(seededConversation.subject) + } Log.d(STEP_TAG, "Navigate to 'STARRED' scope and assert that the conversations is displayed there.") inboxPage.filterInbox("Starred") @@ -346,7 +355,10 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG, "Navigate to 'STARRED' scope. Assert that the conversation is displayed in the 'STARRED' scope.") inboxPage.filterInbox("Starred") - inboxPage.assertConversationDisplayed(seededConversation.subject) + + retry(times = 10, delay = 3000) { + inboxPage.assertConversationDisplayed(seededConversation.subject) + } Log.d(STEP_TAG, "Swipe '${seededConversation.subject}' left and assert it is removed from the 'STARRED' scope because it has became unstarred.") inboxPage.swipeConversationLeft(seededConversation) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt index 03c3a51ddc..374e00d019 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/LoginE2ETest.kt @@ -300,6 +300,8 @@ class LoginE2ETest : StudentTest() { private fun loginWithUser(user: CanvasUserApiModel, lastSchoolSaved: Boolean = false) { + Thread.sleep(5100) //Need to wait > 5 seconds before each login attempt because of new 'too many attempts' login policy on web. + if(lastSchoolSaved) { Log.d(STEP_TAG,"Click 'Find Another School' button.") loginLandingPage.clickFindAnotherSchoolButton() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt index 2c98efb118..4968c7105f 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/TodoE2ETest.kt @@ -15,6 +15,7 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 +import com.instructure.espresso.retry import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -71,12 +72,13 @@ class TodoE2ETest: StudentTest() { dashboardPage.clickTodoTab() Log.d(STEP_TAG,"Assert that ${testAssignment.name} assignment is displayed and ${borderDateAssignment.name} is displayed because it's 7 days away from now..") - todoPage.assertAssignmentDisplayed(testAssignment) - todoPage.assertAssignmentDisplayed(borderDateAssignment) - Log.d(STEP_TAG,"Assert that ${quiz.title} quiz is displayed and ${tooFarAwayQuiz.title} quiz is not displayed because it's end date is more than a week away..") - todoPage.assertQuizDisplayed(quiz) - todoPage.assertQuizNotDisplayed(tooFarAwayQuiz) + retry(times = 5, delay = 3000, catchBlock = { refresh() } ) { + todoPage.assertAssignmentDisplayed(testAssignment) + todoPage.assertAssignmentDisplayed(borderDateAssignment) + todoPage.assertQuizDisplayed(quiz) + todoPage.assertQuizNotDisplayed(tooFarAwayQuiz) + } Log.d(PREPARATION_TAG,"Submit ${testAssignment.name} assignment for ${student.name} student.") SubmissionsApi.seedAssignmentSubmission(SubmissionsApi.SubmissionSeedRequest( diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt index 5ca6d91392..3d300a2e1a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/ManageOfflineContentE2ETest.kt @@ -24,7 +24,6 @@ import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData -import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils import com.instructure.student.ui.utils.StudentTest import com.instructure.student.ui.utils.seedData import com.instructure.student.ui.utils.tokenLogin @@ -48,7 +47,6 @@ class ManageOfflineContentE2ETest : StudentTest() { val student = data.studentsList[0] val course1 = data.coursesList[0] val course2 = data.coursesList[1] - val testAnnouncement = data.announcementsList[0] Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) @@ -131,7 +129,7 @@ class ManageOfflineContentE2ETest : StudentTest() { manageOfflineContentPage.assertCheckedStateOfItem("Announcements", MaterialCheckBox.STATE_UNCHECKED) manageOfflineContentPage.assertCheckedStateOfItem("Discussions", MaterialCheckBox.STATE_UNCHECKED) - Log.d(STEP_TAG, "Assert that the 'SELECT ALL' will be displayed after clicking the 'SELECT ALL' button.") + Log.d(STEP_TAG, "Assert that the 'SELECT ALL' will be displayed after clicking the 'DESELECT ALL' button.") manageOfflineContentPage.assertSelectButtonText(selectAll = true) Log.d(STEP_TAG, "Navigate back to Dashboard Page. Open 'Global' Manage Offline Content page.") @@ -185,7 +183,7 @@ class ManageOfflineContentE2ETest : StudentTest() { Log.d(STEP_TAG, "Assert that both of the seeded courses are displayed as a selectable item in the Manage Offline Content page.") manageOfflineContentPage.assertCourseCountWithMatcher(2) - Log.d(STEP_TAG, "Click on the 'Sync' button.") + Log.d(STEP_TAG, "Click on the 'Sync' button and confirm sync.") manageOfflineContentPage.clickOnSyncButtonAndConfirm() Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") @@ -198,7 +196,7 @@ class ManageOfflineContentE2ETest : StudentTest() { dashboardPage.waitForSyncProgressStartingNotificationToDisappear() Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") - OfflineTestUtils.turnOffConnectionViaADB() + turnOffConnectionViaADB() dashboardPage.waitForRender() Log.d(STEP_TAG, "Select '${course2.name}' course and open 'Grades' menu to check if it's really synced and can be seen in offline mode.") @@ -212,7 +210,7 @@ class ManageOfflineContentE2ETest : StudentTest() { @After fun tearDown() { Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.") - OfflineTestUtils.turnOnConnectionViaADB() + turnOnConnectionViaADB() } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt new file mode 100644 index 0000000000..04a6fc4337 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineCourseBrowserE2ETest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.student.ui.e2e.offline + +import android.util.Log +import androidx.test.espresso.Espresso +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.instructure.canvas.espresso.OfflineE2E +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.student.ui.pages.CourseBrowserPage +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Test +import java.lang.Thread.sleep + +@HiltAndroidTest +class OfflineCourseBrowserE2ETest : StudentTest() { + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @OfflineE2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E) + fun testOfflineCourseBrowserPageUnavailableE2E() { + + Log.d(PREPARATION_TAG,"Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1, announcements = 1) + val student = data.studentsList[0] + val course1 = data.coursesList[0] + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.") + dashboardPage.openGlobalManageOfflineContentPage() + + Log.d(STEP_TAG, "Select the entire '${course1.name}' course for sync. Click on the 'Sync' button.") + Log.d(STEP_TAG, "Expand '${course1.name}' course. Select only the 'Announcements' of the '${course1.name}' course. Click on the 'Sync' button and confirm the sync process.") + manageOfflineContentPage.expandCollapseItem(course1.name) + manageOfflineContentPage.changeItemSelectionState("Announcements") + manageOfflineContentPage.clickOnSyncButtonAndConfirm() + + Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") + dashboardPage.waitForRender() + dashboardPage.waitForSyncProgressDownloadStartedNotification() + dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear() + + Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)") + dashboardPage.waitForSyncProgressStartingNotification() + dashboardPage.waitForSyncProgressStartingNotificationToDisappear() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + turnOffConnectionViaADB() + + Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.") + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Select '${course1.name}' course and open 'Announcements' menu.") + sleep(5000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script. + dashboardPage.selectCourse(course1) + + Log.d(STEP_TAG, "Assert that only the 'Announcements' tab is enabled because it is the only one which has been synced, and assert that all the other, previously synced tabs are disabled, because they weren't synced now.") + var enabledTabs = arrayOf("Announcements") + var disabledTabs = arrayOf("Discussions", "Grades", "People", "Syllabus", "BigBlueButton") + assertTabsEnabled(courseBrowserPage, enabledTabs) + assertTabsDisabled(courseBrowserPage, disabledTabs) + + Log.d(STEP_TAG, "Navigate back to Dashboard Page.Turn back on the Wi-Fi and Mobile Data on the device, and wait for it to come online.") + Espresso.pressBack() + turnOnConnectionViaADB() + dashboardPage.waitForNetworkComeBack() + + Log.d(STEP_TAG, "Open global 'Manage Offline Content' page via the more menu of the Dashboard Page.") + dashboardPage.openGlobalManageOfflineContentPage() + + Log.d(STEP_TAG, "Deselect the entire '${course1.name}' course for sync.") + manageOfflineContentPage.changeItemSelectionState(course1.name) + manageOfflineContentPage.clickOnSyncButtonAndConfirm() + + Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") + dashboardPage.waitForRender() + dashboardPage.waitForSyncProgressDownloadStartedNotification() + dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear() + + Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)") + dashboardPage.waitForSyncProgressStartingNotification() + dashboardPage.waitForSyncProgressStartingNotificationToDisappear() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + turnOffConnectionViaADB() + + Log.d(STEP_TAG, "Select '${course1.name}' course and open 'Announcements' menu.") + sleep(10000) //Need to wait a bit here because of a UI glitch that when network state change, the dashboard page 'pops' a bit and it can confuse the automation script. + device.waitForIdle() + device.waitForWindowUpdate(null, 10000) + dashboardPage.selectCourse(course1) + + Log.d(STEP_TAG, "Assert that the 'Google Drive' and 'Collaborations' tabs are disabled because they aren't supported in offline mode, but the rest of the tabs are enabled because the whole course has been synced.") + enabledTabs = arrayOf("Announcements", "Discussions", "Grades", "People", "Syllabus", "BigBlueButton") + disabledTabs = arrayOf("Google Drive", "Collaborations") + assertTabsEnabled(courseBrowserPage, enabledTabs) + assertTabsDisabled(courseBrowserPage, disabledTabs) + } + + @After + fun tearDown() { + Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device, so it will come back online.") + turnOnConnectionViaADB() + } + + private fun assertTabsEnabled(courseBrowserPage: CourseBrowserPage, tabs: Array) { + tabs.forEach { tab -> + courseBrowserPage.assertTabEnabled(tab) + } + } + + private fun assertTabsDisabled(courseBrowserPage: CourseBrowserPage, tabs: Array) { + tabs.forEach { tab -> + courseBrowserPage.assertTabDisabled(tab) + } + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt similarity index 52% rename from apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt index 599a31aa0a..21a3fa625b 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/DashboardE2EOfflineTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineDashboardE2ETest.kt @@ -17,9 +17,12 @@ package com.instructure.student.ui.e2e.offline import android.util.Log +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice import com.instructure.canvas.espresso.OfflineE2E import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.SecondaryFeatureCategory import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils @@ -31,7 +34,7 @@ import org.junit.After import org.junit.Test @HiltAndroidTest -class DashboardE2EOfflineTest : StudentTest() { +class OfflineDashboardE2ETest : StudentTest() { override fun displaysPageObjects() = Unit override fun enableAndConfigureAccessibilityChecks() = Unit @@ -40,6 +43,7 @@ class DashboardE2EOfflineTest : StudentTest() { @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E) fun testOfflineDashboardE2E() { + Log.d(PREPARATION_TAG,"Seeding data.") val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1) val student = data.studentsList[0] @@ -47,6 +51,9 @@ class DashboardE2EOfflineTest : StudentTest() { val course2 = data.coursesList[1] val testAnnouncement = data.announcementsList[0] + Log.d(PREPARATION_TAG, "Get the device to be able to perform app-independent actions on it.") + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") tokenLogin(student) dashboardPage.waitForRender() @@ -68,7 +75,8 @@ class DashboardE2EOfflineTest : StudentTest() { dashboardPage.waitForSyncProgressStartingNotificationToDisappear() Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") - OfflineTestUtils.turnOffConnectionViaADB() + turnOffConnectionViaADB() + device.waitForIdle() Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.") dashboardPage.waitForRender() @@ -76,7 +84,7 @@ class DashboardE2EOfflineTest : StudentTest() { Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.") OfflineTestUtils.assertOfflineIndicator() - Log.d(STEP_TAG, "Assert that the offline sync icon only displayed on the synced course's cours card.") + Log.d(STEP_TAG, "Assert that the offline sync icon only displayed on the synced course's course card.") dashboardPage.assertCourseOfflineSyncIconVisible(course1.name) dashboardPage.assertCourseOfflineSyncIconGone(course2.name) @@ -88,10 +96,68 @@ class DashboardE2EOfflineTest : StudentTest() { announcementListPage.assertTopicDisplayed(testAnnouncement.title) } + @OfflineE2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.DASHBOARD, TestCategory.E2E, false, SecondaryFeatureCategory.OFFLINE_MODE) + fun testOfflineDashboardUnavailableFeaturesE2E() { + + Log.d(PREPARATION_TAG,"Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 1) + val student = data.studentsList[0] + val course = data.coursesList[0] + + Log.d(PREPARATION_TAG, "Get the device to be able to perform app-independent actions on it.") + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open the '${course.name}' course's 'Manage Offline Content' page via the more menu of the Dashboard Page.") + dashboardPage.clickCourseOverflowMenu(course.name, "Manage Offline Content") + + Log.d(STEP_TAG, "Select the entire '${course.name}' course for sync. Click on the 'Sync' button.") + manageOfflineContentPage.changeItemSelectionState(course.name) + manageOfflineContentPage.clickOnSyncButtonAndConfirm() + + Log.d(STEP_TAG, "Wait for the 'Download Started' dashboard notification to be displayed, and the to disappear.") + dashboardPage.waitForRender() + dashboardPage.waitForSyncProgressDownloadStartedNotification() + dashboardPage.waitForSyncProgressDownloadStartedNotificationToDisappear() + + Log.d(STEP_TAG, "Wait for the 'Syncing Offline Content' dashboard notification to be displayed, and the to disappear. (It should be displayed after the 'Download Started' notification immediately.)") + dashboardPage.waitForSyncProgressStartingNotification() + dashboardPage.waitForSyncProgressStartingNotificationToDisappear() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + turnOffConnectionViaADB() + device.waitForIdle() + + Log.d(STEP_TAG, "Wait for the Dashboard Page to be rendered. Refresh the page.") + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.") + OfflineTestUtils.assertOfflineIndicator() + + Log.d(STEP_TAG, "Assert that the bottom menus (except Dashboard) are disabled and unavailable in offline mode.") + dashboardPage.assertBottomMenusAreDisabled() + + Log.d(STEP_TAG, "Try to open the '${course.name}' course's more menu of the Dashboard Page. Assert that the 'No Internet Connection' dialog is displayed. Dismiss it after the assertion.") + dashboardPage.clickOnCourseOverflowButton(course.name) + OfflineTestUtils.assertNoInternetConnectionDialog() + OfflineTestUtils.dismissNoInternetConnectionDialog() + + Log.d(STEP_TAG, "Try to open the global 'Manage Offline Content' page via the more menu of the Dashboard Page. Assert that the 'No Internet Connection' dialog is displayed. Dismiss it after the assertion.") + Thread.sleep(5000) //Wait for the system notification to disappear, because it overlaps the More menu button on the toolbar. + dashboardPage.openGlobalManageOfflineContentPage() + OfflineTestUtils.assertNoInternetConnectionDialog() + OfflineTestUtils.dismissNoInternetConnectionDialog() + } + @After fun tearDown() { Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device, so it will come back online.") - OfflineTestUtils.turnOnConnectionViaADB() + turnOnConnectionViaADB() } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt index 77cea19bc2..26637e2f1a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncProgressE2ETest.kt @@ -80,7 +80,7 @@ class OfflineSyncProgressE2ETest : StudentTest() { Espresso.pressBack() Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") - OfflineTestUtils.turnOffConnectionViaADB() + turnOffConnectionViaADB() dashboardPage.waitForRender() Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Dashboard Page.") @@ -101,7 +101,7 @@ class OfflineSyncProgressE2ETest : StudentTest() { @After fun tearDown() { Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.") - OfflineTestUtils.turnOnConnectionViaADB() + turnOnConnectionViaADB() } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt new file mode 100644 index 0000000000..107d91c4c6 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyncSettingsE2ETest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.student.ui.e2e.offline + +import android.util.Log +import com.instructure.canvas.espresso.OfflineE2E +import com.instructure.panda_annotations.FeatureCategory +import com.instructure.panda_annotations.Priority +import com.instructure.panda_annotations.TestCategory +import com.instructure.panda_annotations.TestMetaData +import com.instructure.pandautils.R +import com.instructure.student.ui.utils.StudentTest +import com.instructure.student.ui.utils.ViewUtils +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Test + +@HiltAndroidTest +class OfflineSyncSettingsE2ETest : StudentTest() { + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @OfflineE2E + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.OFFLINE_CONTENT, TestCategory.E2E) + fun offlineSyncSettingsE2ETest() { + + Log.d(PREPARATION_TAG,"Seeding data.") + val data = seedData(students = 1, teachers = 1, courses = 2, announcements = 1) + val student = data.studentsList[0] + + Log.d(STEP_TAG,"Login with user: ${student.name}, login id: ${student.loginId}.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open Settings page from the Left Side menu.") + leftSideNavigationDrawerPage.clickSettingsMenu() + + Log.d(STEP_TAG, "Assert that the Offline Sync Settings related information is displayed properly on the Settings Page ('Daily' is the default status).") + settingsPage.assertOfflineContentDisplayed() + settingsPage.assertOfflineContentTitle() + settingsPage.assertOfflineSyncSettingsStatus(R.string.daily) + + Log.d(STEP_TAG, "Open Offline Sync Settings page and wait for it to be loaded.") + settingsPage.openOfflineSyncSettingsPage() + offlineSyncSettingsPage.assertPageObjects() + + Log.d(STEP_TAG, "Assert that further settings, such as the toolbar title is displayed and correct, and both the Auto Content Sync and Wi-Fi Only Sync toggles are displayed and checked by default.") + offlineSyncSettingsPage.assertFurtherSettingsIsDisplayed() + offlineSyncSettingsPage.assertSyncSettingsToolbarTitle() + offlineSyncSettingsPage.assertAutoSyncSwitchIsChecked() + offlineSyncSettingsPage.assertWifiOnlySwitchIsChecked() + + Log.d(STEP_TAG, "Assert that all the descriptions of how these settings are working are displayed.") + offlineSyncSettingsPage.assertSyncSettingsPageDescriptions() + + Log.d(STEP_TAG, "Assert that the sync frequency label is 'Daily', because that is the default setting.") + offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.daily) + + Log.d(STEP_TAG, "Switch off the 'Auto Content Sync' toggle, and assert if that the further settings below will disappear.") + offlineSyncSettingsPage.clickAutoSyncSwitch() + + Log.d(STEP_TAG, "Assert that further settings, such as the toolbar title, Auto Content Sync and Wi-Fi Only Sync toggles are NOT displayed.") + offlineSyncSettingsPage.assertFurtherSettingsNotDisplayed() + + Log.d(STEP_TAG, "Switch back the 'Auto Content Sync' toggle, and assert if that the further settings below will be displayed again.") + offlineSyncSettingsPage.clickAutoSyncSwitch() + + Log.d(STEP_TAG, "Assert that further settings, such as the toolbar title is displayed and correct, and both the Auto Content Sync and Wi-Fi Only Sync toggles are displayed again.") + offlineSyncSettingsPage.assertFurtherSettingsIsDisplayed() + + Log.d(STEP_TAG, "Switch off the 'Sync Content Wi-Fi Only' toggle and assert that the confirmation dialog (with the proper texts) is displayed.") + offlineSyncSettingsPage.clickWifiOnlySwitch() + offlineSyncSettingsPage.assertTurnOffWifiOnlyDialogTexts() + + Log.d(STEP_TAG, "Click on the 'TURN OFF' button on the dialog to really turn off the 'Sync Content Wi-Fi Only' switch.") + offlineSyncSettingsPage.clickTurnOff() + + Log.d(STEP_TAG, "Assert that the 'Sync Content Wi-Fi Only' switch is not checked any more.") + offlineSyncSettingsPage.assertWifiOnlySwitchIsNotChecked() + + Log.d(STEP_TAG, "Open the Sync Frequency Settings dialog and select 'Weekly' option.") + offlineSyncSettingsPage.openSyncFrequencySettingsDialog() + offlineSyncSettingsPage.clickSyncFrequencyDialogOption(R.string.weekly) + + Log.d(STEP_TAG, "Assert that the sync frequency label became 'Weekly' (without any manual refresh).") + offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly) + + Log.d(STEP_TAG, "Navigate back to Dashboard Page and logout.") + ViewUtils.pressBackButton(2) + leftSideNavigationDrawerPage.logout() + + Log.d(STEP_TAG, "Click 'Find My School' button.") + loginLandingPage.clickFindMySchoolButton() + + Log.d(STEP_TAG, "Enter domain: ${student.domain}.") + loginFindSchoolPage.enterDomain(student.domain) + + Log.d(STEP_TAG, "Click on 'Next' button on the Toolbar.") + loginFindSchoolPage.clickToolbarNextMenuItem() + + Log.d(STEP_TAG, "Login with user: ${student.name}, login id: ${student.loginId}.") + loginSignInPage.loginAs(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open Settings page from the Left Side menu.") + leftSideNavigationDrawerPage.clickSettingsMenu() + + Log.d(STEP_TAG, "Assert that the Offline Sync Settings frequency text is 'Weekly' (because we set it previously).") + settingsPage.assertOfflineSyncSettingsStatus(R.string.weekly) + + Log.d(STEP_TAG, "Open Offline Sync Settings page and wait for it to be loaded.") + settingsPage.openOfflineSyncSettingsPage() + offlineSyncSettingsPage.assertPageObjects() + + Log.d(STEP_TAG, "Assert that the Offline Sync Settings frequency text is 'Weekly' (because we set it previously) and the 'Sync Content Wi-Fi Only' switch is switched off.") + offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly) + offlineSyncSettingsPage.assertWifiOnlySwitchIsNotChecked() + } + + @After + fun tearDown() { + Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device via ADB, so it will come back online.") + turnOnConnectionViaADB() + } + +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt index 717d37af98..0696eec9b3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/utils/OfflineTestUtils.kt @@ -16,14 +16,14 @@ */ package com.instructure.student.ui.e2e.offline.utils -import androidx.test.espresso.matcher.ViewMatchers.withChild -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiSelector import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.click import com.instructure.espresso.matchers.WaitForViewMatcher.waitForView import com.instructure.espresso.page.plus import com.instructure.student.R @@ -31,18 +31,6 @@ import org.hamcrest.CoreMatchers.allOf object OfflineTestUtils { - fun turnOffConnectionViaADB() { - val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - device.executeShellCommand("svc wifi disable") - device.executeShellCommand("svc data disable") - } - - fun turnOnConnectionViaADB() { - val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - device.executeShellCommand("svc wifi enable") - device.executeShellCommand("svc data enable") - } - fun turnOffConnectionOnUI() { val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) @@ -80,4 +68,14 @@ object OfflineTestUtils { ) ).assertDisplayed() } + + fun assertNoInternetConnectionDialog() { + waitForView(withId(R.id.alertTitle) + withText(R.string.noInternetConnectionTitle)).assertDisplayed() + } + + fun dismissNoInternetConnectionDialog() { + onView(withText(android.R.string.ok) + isDescendantOfA(withId(R.id.buttonPanel) + + hasSibling(withId(R.id.topPanel) + + hasDescendant(withText(R.string.noInternetConnectionTitle))))).click() + } } \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt index 57590587ec..28f9d111fb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/OfflineContentInteractionTest.kt @@ -367,9 +367,9 @@ class OfflineContentInteractionTest : StudentTest() { tokenLogin(data.domain, token, student) dashboardPage.waitForRender() leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openOfflineContentPage() - syncSettingsPage.clickWifiOnlySwitch() - syncSettingsPage.clickTurnOff() + settingsPage.openOfflineSyncSettingsPage() + offlineSyncSettingsPage.clickWifiOnlySwitch() + offlineSyncSettingsPage.clickTurnOff() Espresso.pressBack() Espresso.pressBack() dashboardPage.openGlobalManageOfflineContentPage() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt index e81a192070..319772e5f3 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyncSettingsInteractionTest.kt @@ -38,36 +38,36 @@ class SyncSettingsInteractionTest : StudentTest() { @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) fun testFurtherSettingsDisplayedByDefault() { goToSyncSettings() - syncSettingsPage.assertFurtherSettingsIsDisplayed() + offlineSyncSettingsPage.assertFurtherSettingsIsDisplayed() } @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) fun testClickAutoSyncHidesFurtherSettings() { goToSyncSettings() - syncSettingsPage.clickAutoSyncSwitch() - syncSettingsPage.assertFurtherSettingsNotDisplayed() + offlineSyncSettingsPage.clickAutoSyncSwitch() + offlineSyncSettingsPage.assertFurtherSettingsNotDisplayed() } @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) fun testChangeFrequency() { goToSyncSettings() - syncSettingsPage.assertFrequencyLabelText(R.string.daily) - syncSettingsPage.clickFrequency() - syncSettingsPage.clickDialogOption(R.string.weekly) - syncSettingsPage.assertFrequencyLabelText(R.string.weekly) + offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.daily) + offlineSyncSettingsPage.openSyncFrequencySettingsDialog() + offlineSyncSettingsPage.clickSyncFrequencyDialogOption(R.string.weekly) + offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly) } @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.SYNC_SETTINGS, TestCategory.INTERACTION, false) fun testChangeContentOverWifiOnly() { goToSyncSettings() - syncSettingsPage.assertWifiOnlySwitchIsChecked() - syncSettingsPage.clickWifiOnlySwitch() - syncSettingsPage.assertDialogDisplayedWithTitle(R.string.syncSettings_wifiConfirmationTitle) - syncSettingsPage.clickTurnOff() - syncSettingsPage.assertWifiOnlySwitchIsNotChecked() + offlineSyncSettingsPage.assertWifiOnlySwitchIsChecked() + offlineSyncSettingsPage.clickWifiOnlySwitch() + offlineSyncSettingsPage.assertTurnOffWifiOnlyDialogTexts() + offlineSyncSettingsPage.clickTurnOff() + offlineSyncSettingsPage.assertWifiOnlySwitchIsNotChecked() } @Test @@ -76,10 +76,10 @@ class SyncSettingsInteractionTest : StudentTest() { val data = createMockCanvas() goToSyncSettings(data) - syncSettingsPage.clickFrequency() - syncSettingsPage.clickDialogOption(R.string.weekly) - syncSettingsPage.clickWifiOnlySwitch() - syncSettingsPage.clickTurnOff() + offlineSyncSettingsPage.openSyncFrequencySettingsDialog() + offlineSyncSettingsPage.clickSyncFrequencyDialogOption(R.string.weekly) + offlineSyncSettingsPage.clickWifiOnlySwitch() + offlineSyncSettingsPage.clickTurnOff() with(activityRule) { finishActivity() @@ -88,8 +88,8 @@ class SyncSettingsInteractionTest : StudentTest() { goToSyncSettings(data) - syncSettingsPage.assertFrequencyLabelText(R.string.weekly) - syncSettingsPage.assertWifiOnlySwitchIsNotChecked() + offlineSyncSettingsPage.assertSyncFrequencyLabelText(R.string.weekly) + offlineSyncSettingsPage.assertWifiOnlySwitchIsNotChecked() } private fun createMockCanvas(): MockCanvas { @@ -104,6 +104,6 @@ class SyncSettingsInteractionTest : StudentTest() { tokenLogin(data.domain, token, student) dashboardPage.waitForRender() leftSideNavigationDrawerPage.clickSettingsMenu() - settingsPage.openOfflineContentPage() + settingsPage.openOfflineSyncSettingsPage() } } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt index d3a9a95924..87cbc0de71 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/CourseBrowserPage.kt @@ -17,12 +17,15 @@ package com.instructure.student.ui.pages import android.view.View +import android.widget.LinearLayout +import androidx.constraintlayout.widget.ConstraintLayout import androidx.test.espresso.Espresso.onView import androidx.test.espresso.PerformException import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.* import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvas.espresso.withCustomConstraints @@ -34,11 +37,15 @@ import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.assertHasText import com.instructure.espresso.click import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.plus +import com.instructure.espresso.scrollTo import com.instructure.espresso.swipeUp import com.instructure.pandautils.views.SwipeRefreshLayoutAppBar import com.instructure.student.R import org.hamcrest.Matcher import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.anyOf +import org.hamcrest.Matchers.not open class CourseBrowserPage : BasePage(R.id.courseBrowserPage) { @@ -140,6 +147,15 @@ open class CourseBrowserPage : BasePage(R.id.courseBrowserPage) { onView(allOf(withText(tabTitle), withId(R.id.label))).check(doesNotExist()) } + //OfflineMethod + fun assertTabDisabled(tabTitle: String) { + onView(allOf(anyOf(isAssignableFrom(LinearLayout::class.java), isAssignableFrom(ConstraintLayout::class.java)), withChild(anyOf(withId(R.id.label), withId(R.id.unsupportedLabel)) + withText(tabTitle)))).scrollTo().check(matches(not(isEnabled()))) + } + + fun assertTabEnabled(tabTitle: String) { + onView(allOf(anyOf(isAssignableFrom(LinearLayout::class.java), isAssignableFrom(ConstraintLayout::class.java)), withChild(anyOf(withId(R.id.label), withId(R.id.unsupportedLabel)) + withText(tabTitle)))).scrollTo().check(matches(isEnabled())) + } + // Minimizes toolbar if it is not already minimized private fun minimizeToolbar() { try { diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt index 78121a4c7e..c4d2b55143 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt @@ -38,6 +38,7 @@ import com.instructure.canvasapi2.models.Group import com.instructure.dataseeding.model.CourseApiModel import com.instructure.dataseeding.model.GroupApiModel import com.instructure.espresso.* +import com.instructure.espresso.matchers.WaitForViewMatcher.waitForViewToBeCompletelyDisplayed import com.instructure.espresso.page.* import com.instructure.student.R import com.instructure.student.ui.utils.ViewUtils @@ -255,18 +256,22 @@ class DashboardPage : BasePage(R.id.dashboardPage) { } fun switchCourseView() { - Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext) + clickDashboardGlobalOverflowButton() onView(withText(containsString("Switch to"))) .perform(click()); } - //OfflineMethod fun openGlobalManageOfflineContentPage() { - Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext) - onView(withText(containsString("Manage Offline Content"))) + clickDashboardGlobalOverflowButton() + onView(withText(containsString("Manage Offline Content"))) .perform(click()); } + private fun clickDashboardGlobalOverflowButton() { + waitForViewToBeCompletelyDisplayed(withContentDescription("More options") + withAncestor(R.id.toolbar)) + Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext) + } + fun clickEditDashboard() { onView(withId(R.id.editDashboardTextView)).scrollTo().click() } @@ -304,12 +309,16 @@ class DashboardPage : BasePage(R.id.dashboardPage) { } fun clickCourseOverflowMenu(courseTitle: String, menuTitle: String) { + clickOnCourseOverflowButton(courseTitle) + waitForView(withId(R.id.title) + withText(menuTitle)).click() + } + + fun clickOnCourseOverflowButton(courseTitle: String) { val courseOverflowMatcher = withId(R.id.overflow) + withAncestor( withId(R.id.cardView) + withDescendant(withId(R.id.titleTextView) + withText(courseTitle)) ) waitForView(courseOverflowMatcher).scrollTo().click() - waitForView(withId(R.id.title) + withText(menuTitle)).click() } fun assertCourseGrade(courseName: String, courseGrade: String) { @@ -345,6 +354,22 @@ class DashboardPage : BasePage(R.id.dashboardPage) { onView(withId(R.id.offlineIndicator)).check(matches(withEffectiveVisibility(Visibility.GONE))) } + //OfflineMethod + fun waitForNetworkComeBack() { + assertDisplaysCourses() + retry(times = 5, delay = 2000) { + assertOfflineIndicatorNotDisplayed() + } + } + + //OfflineMethod + fun waitForNetworkOff() { + assertDisplaysCourses() + retry(times = 5, delay = 2000) { + assertOfflineIndicatorDisplayed() + } + } + //OfflineMethod fun assertCourseOfflineSyncIconVisible(courseName: String) { waitForView(withId(R.id.offlineSyncIcon) + hasSibling(withId(R.id.titleTextView) + withText(courseName))).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) @@ -379,6 +404,14 @@ class DashboardPage : BasePage(R.id.dashboardPage) { fun waitForSyncProgressStartingNotificationToDisappear() { ViewUtils.waitForViewToDisappear(withText(com.instructure.pandautils.R.string.syncProgress_syncingOfflineContent), 30) } + + //OfflineMethod + fun assertBottomMenusAreDisabled() { + onView(withId(R.id.bottomNavigationCalendar)).check(matches(isNotEnabled())) + onView(withId(R.id.bottomNavigationToDo)).check(matches(isNotEnabled())) + onView(withId(R.id.bottomNavigationNotifications)).check(matches(isNotEnabled())) + onView(withId(R.id.bottomNavigationInbox)).check(matches(isNotEnabled())) + } } /** diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt index 47ac688d03..8626f8d2a0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt @@ -145,7 +145,6 @@ class InboxPage : BasePage(R.id.inboxPage) { hasSibling(allOf(withId(R.id.subjectView), withText(subject)))) waitForMatcherWithRefreshes(matcher) // May need to refresh before the star shows up onView(matcher).scrollTo().assertDisplayed() - } fun assertConversationNotStarred(subject: String) { @@ -154,9 +153,7 @@ class InboxPage : BasePage(R.id.inboxPage) { hasSibling(withId(R.id.userName)), hasSibling(withId(R.id.date)), hasSibling(allOf(withId(R.id.subjectView), withText(subject)))) - waitForMatcherWithRefreshes(matcher) // May need to refresh before the star shows up onView(matcher).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) - } fun assertUnreadMarkerVisibility(conversation: Conversation, visibility: ViewMatchers.Visibility) { @@ -192,7 +189,7 @@ class InboxPage : BasePage(R.id.inboxPage) { } } - fun assertInboxEmpty() { + fun assertInboxEmpty() { waitForView(withId(R.id.emptyInboxView)).assertDisplayed() } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt index 8f40d0d703..0792369df0 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SettingsPage.kt @@ -16,12 +16,22 @@ */ package com.instructure.student.ui.pages -import com.instructure.espresso.* -import com.instructure.espresso.page.* +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.TextViewColorAssertion +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.assertNotDisplayed +import com.instructure.espresso.click +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withParent +import com.instructure.espresso.page.withText +import com.instructure.espresso.scrollTo import com.instructure.student.R class SettingsPage : BasePage(R.id.settingsFragment) { - private val toolbar by OnViewWithId(R.id.toolbar) private val profileSettingLabel by OnViewWithId(R.id.profileSettings) private val accountPreferencesLabel by OnViewWithId(R.id.accountPreferences) private val pushNotificationsLabel by OnViewWithId(R.id.pushNotifications) @@ -81,15 +91,30 @@ class SettingsPage : BasePage(R.id.settingsFragment) { appThemeStatus.check(TextViewColorAssertion(expectedTextColor)) } - fun openOfflineContentPage() { + //OfflineMethod + fun openOfflineSyncSettingsPage() { offlineContent.scrollTo().click() } + //OfflineMethod fun assertOfflineContentDisplayed() { offlineContent.scrollTo().assertDisplayed() } + //OfflineMethod fun assertOfflineContentNotDisplayed() { offlineContent.assertNotDisplayed() } + + //OfflineMethod + fun assertOfflineContentTitle() { + onView(withId(R.id.offlineContentTitle) + withText(R.string.offlineContent)).assertDisplayed() + } + + //OfflineMethod + fun assertOfflineSyncSettingsStatus(expectedStatus: Int) { + onView(withId(R.id.offlineSyncSettingsStatus) + withText(expectedStatus) + withParent(R.id.offlineSyncSettingsContainer) + + hasSibling(withId(R.id.offlineSyncSettingsTitle) + withText(R.string.offlineSyncSettingsTitle))).assertDisplayed() + } + } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt deleted file mode 100644 index a7f2db9f3b..0000000000 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyncSettingsPage.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2023 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package com.instructure.student.ui.pages - -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isChecked -import androidx.test.espresso.matcher.ViewMatchers.isNotChecked -import com.instructure.espresso.* -import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onViewWithText -import com.instructure.pandautils.R - -class SyncSettingsPage : BasePage(R.id.syncSettingsPage) { - - private val toolbar by OnViewWithId(R.id.toolbar) - private val autoSyncSwitch by OnViewWithId(R.id.autoSyncSwitch) - private val furtherSettings by OnViewWithId(R.id.furtherSettings) - private val syncFrequencyLabel by OnViewWithId(R.id.syncFrequencyLabel) - private val wifiOnlySwitch by OnViewWithId(R.id.wifiOnlySwitch) - - fun clickAutoSyncSwitch() { - autoSyncSwitch.click() - } - - fun clickFrequency() { - syncFrequencyLabel.click() - } - - fun clickDialogOption(stringResId: Int) { - onViewWithText(stringResId).click() - } - - fun clickWifiOnlySwitch() { - wifiOnlySwitch.click() - } - - fun clickTurnOff() { - onViewWithText(R.string.syncSettings_wifiConfirmationPositiveButton).click() - } - - fun assertFurtherSettingsIsDisplayed() { - furtherSettings.assertDisplayed() - } - - fun assertFurtherSettingsNotDisplayed() { - furtherSettings.assertNotDisplayed() - } - - fun assertFrequencyLabelText(expected: Int) { - syncFrequencyLabel.assertHasText(expected) - } - - fun assertWifiOnlySwitchIsChecked() { - wifiOnlySwitch.check(matches(isChecked())) - } - - fun assertWifiOnlySwitchIsNotChecked() { - wifiOnlySwitch.check(matches(isNotChecked())) - } - - fun assertDialogDisplayedWithTitle(title: Int) { - onViewWithText(title).assertDisplayed() - } -} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt index 80887424eb..184b5ba3a4 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/ManageOfflineContentPage.kt @@ -17,14 +17,34 @@ package com.instructure.student.ui.pages.offline +import androidx.test.espresso.Espresso +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.Visibility +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers.* import com.instructure.canvas.espresso.containsTextCaseInsensitive import com.instructure.canvas.espresso.hasCheckedState import com.instructure.canvas.espresso.withRotation -import com.instructure.espresso.* +import com.instructure.espresso.ConstraintLayoutItemCountAssertion +import com.instructure.espresso.ConstraintLayoutItemCountAssertionWithMatcher +import com.instructure.espresso.DoesNotExistAssertion +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.WaitForViewWithId import com.instructure.espresso.actions.ForceClick -import com.instructure.espresso.page.* +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.click +import com.instructure.espresso.matchers.WaitForViewMatcher +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.waitForView +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withParent +import com.instructure.espresso.page.withText +import com.instructure.espresso.scrollTo import com.instructure.pandautils.R import com.instructure.pandautils.binding.BindableViewHolder import org.hamcrest.CoreMatchers.allOf @@ -34,45 +54,37 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { private val syncButton by OnViewWithId(R.id.syncButton) private val storageInfoContainer by WaitForViewWithId(R.id.storageInfoContainer) - //OfflineMethod fun changeItemSelectionState(itemName: String) { onView(withId(R.id.offlineContentRecyclerView)) .perform(RecyclerViewActions.scrollTo(hasDescendant(withText(itemName)))) onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().click() } - //OfflineMethod fun expandCollapseItem(itemName: String) { onView(withId(R.id.arrow) + withEffectiveVisibility(Visibility.VISIBLE) + hasSibling(withId(R.id.title) + withText(itemName))).scrollTo().perform(ForceClick()) } - //OfflineMethod fun expandCollapseFiles() { expandCollapseItem("Files") } - //OfflineMethod fun clickOnSyncButton() { syncButton.click() } - //OfflineMethod fun clickOnSyncButtonAndConfirm() { clickOnSyncButton() confirmSync() } - //OfflineMethod private fun confirmSync() { waitForView(withText("Sync") + withAncestor(R.id.buttonPanel)).click() } - //OfflineMethod fun confirmDiscardChanges() { waitForView(withText("Discard") + withAncestor(R.id.buttonPanel)).click() } - //OfflineMethod fun assertStorageInfoDetails() { onView(withId(R.id.storageLabel) + withText(R.string.offline_content_storage)).assertDisplayed() onView(withId(R.id.storageInfo) + containsTextCaseInsensitive("Used")).assertDisplayed() @@ -82,39 +94,32 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { onView(withId(R.id.remainingLabel) + withText(R.string.offline_content_remaining)).assertDisplayed() } - //OfflineMethod fun assertSelectButtonText(selectAll: Boolean) { if (selectAll) waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).assertDisplayed() else waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_deselect_all)).assertDisplayed() } - //OfflineMethod fun clickOnSelectAllButton() { waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_select_all)).click() } - //OfflineMethod fun clickOnDeselectAllButton() { waitForView(withId(R.id.menu_select_all) + withText(R.string.offline_content_deselect_all)).click() } - //OfflineMethod fun assertCourseCountWithMatcher(expectedCount: Int) { ConstraintLayoutItemCountAssertionWithMatcher((allOf(withId(R.id.arrow), withEffectiveVisibility(Visibility.VISIBLE))), expectedCount) } - //OfflineMethod fun assertCourseCount(expectedCount: Int) { onView((allOf(withId(R.id.arrow), withEffectiveVisibility(Visibility.VISIBLE)))).check(ConstraintLayoutItemCountAssertion(expectedCount)) } - //OfflineMethod fun assertToolbarTexts(courseName: String) { onView(withText(courseName) + withParent(R.id.toolbar) + withAncestor(R.id.manageOfflineContentPage)).assertDisplayed() onView(withText(R.string.offline_content_toolbar_title) + withParent(R.id.toolbar) + withAncestor(R.id.manageOfflineContentPage)).assertDisplayed() } - //OfflineMethod fun assertCheckedStateOfItem(itemName: String, state: Int) { val matcher = withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName)) + hasCheckedState(state) onView(withId(R.id.offlineContentRecyclerView)) @@ -122,31 +127,27 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { onView(matcher).scrollTo().assertDisplayed() } - //OfflineMethod fun waitForItemDisappear(itemName: String) { onView(withId(R.id.checkbox) + hasSibling(withId(R.id.title) + withText(itemName))).check(DoesNotExistAssertion(5)) } - //OfflineMethod fun assertDisplaysNoCourses() { - onView(withText(R.string.offline_content_empty_message)).assertDisplayed() + Espresso.onView(ViewMatchers.withText(R.string.offline_content_empty_message)).assertDisplayed() } - //OfflineMethod fun assertDisplaysEmptyCourse() { - onView(withText(R.string.offline_content_empty_course_message)).scrollTo().assertDisplayed() + Espresso.onView(ViewMatchers.withText(R.string.offline_content_empty_course_message)).scrollTo().assertDisplayed() } - //OfflineMethod fun assertDisplaysItemWithExpandedState(title: String, expanded: Boolean) { - onView(withId(R.id.arrow) + Espresso.onView( + ViewMatchers.withId(R.id.arrow) + withRotation(if (expanded) 180f else 0f) + withEffectiveVisibility(Visibility.VISIBLE) - + hasSibling(withId(R.id.title) + withText(title)) + + hasSibling(ViewMatchers.withId(R.id.title) + ViewMatchers.withText(title)) ).scrollTo().assertDisplayed() } - //OfflineMethod fun assertItemDisplayed(title: String) { val matcher = withId(R.id.title) + withText(title) onView(withId(R.id.offlineContentRecyclerView)) @@ -154,19 +155,23 @@ class ManageOfflineContentPage : BasePage(R.id.manageOfflineContentPage) { onView(matcher).scrollTo().assertDisplayed() } - //OfflineMethod fun assertDiscardDialogDisplayed() { - waitForView(withText(R.string.offline_content_discard_dialog_title)).assertDisplayed() + WaitForViewMatcher.waitForView(ViewMatchers.withText(R.string.offline_content_discard_dialog_title)) + .assertDisplayed() } - //OfflineMethod fun assertSyncDialogDisplayed(text: String) { - waitForView(withText(text)).assertDisplayed() + WaitForViewMatcher.waitForView(ViewMatchers.withText(text)).assertDisplayed() } - //OfflineMethod fun assertStorageInfoText(storageInfoText: String) { - onView(withId(R.id.storageInfo) + withText(storageInfoText)).assertDisplayed() + Espresso.onView( + ViewMatchers.withId(R.id.storageInfo) + ViewMatchers.withText( + storageInfoText + ) + ).assertDisplayed() } } + + diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt new file mode 100644 index 0000000000..d27445850e --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/offline/OfflineSyncSettingsPage.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.student.ui.pages.offline + +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.assertHasText +import com.instructure.espresso.assertNotDisplayed +import com.instructure.espresso.click +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.onViewWithText +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.waitForView +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withId +import com.instructure.espresso.page.withParent +import com.instructure.espresso.page.withText +import com.instructure.pandautils.R + +class OfflineSyncSettingsPage : BasePage(R.id.syncSettingsPage) { + + private val toolbar by OnViewWithId(R.id.toolbar) + private val autoSyncSwitch by OnViewWithId(R.id.autoSyncSwitch) + private val furtherSettings by OnViewWithId(R.id.furtherSettings) + private val syncFrequencyLabel by OnViewWithId(R.id.syncFrequencyLabel) + private val wifiOnlySwitch by OnViewWithId(R.id.wifiOnlySwitch) + + fun clickAutoSyncSwitch() { + autoSyncSwitch.click() + } + + fun openSyncFrequencySettingsDialog() { + syncFrequencyLabel.click() + } + + fun clickSyncFrequencyDialogOption(stringResId: Int) { + onView(withText(stringResId) + withParent(R.id.select_dialog_listview)).click() + } + + fun clickWifiOnlySwitch() { + wifiOnlySwitch.click() + } + + fun clickTurnOff() { + onViewWithText(R.string.syncSettings_wifiConfirmationPositiveButton).click() + } + + fun assertTurnOffWifiOnlyDialogTexts() { + waitForView(withId(R.id.alertTitle) + withText(R.string.syncSettings_wifiConfirmationTitle)).assertDisplayed() + waitForView(withText(R.string.syncSettings_wifiConfirmationPositiveButton) + withAncestor(R.id.buttonPanel)).assertDisplayed() + onView(withText(R.string.synySettings_wifiConfirmationMessage)).assertDisplayed() + } + + fun assertFurtherSettingsIsDisplayed() { + furtherSettings.assertDisplayed() + } + + fun assertFurtherSettingsNotDisplayed() { + furtherSettings.assertNotDisplayed() + } + + fun assertSyncFrequencyLabelText(expected: Int) { + syncFrequencyLabel.assertHasText(expected) + } + + fun assertSyncFrequencyTitleText() { + onView(withText(R.string.syncSettings_syncFrequencyTitle) + withParent(R.id.syncFrequencyContainer)).assertDisplayed() + } + + fun assertWifiOnlySwitchIsChecked() { + wifiOnlySwitch.check(matches(isChecked())) + } + + fun assertWifiOnlySwitchIsNotChecked() { + wifiOnlySwitch.check(matches(isNotChecked())) + } + + fun assertAutoSyncSwitchIsChecked() { + autoSyncSwitch.check(matches(isChecked())) + } + + fun assertAutoSyncSwitchIsNotChecked() { + autoSyncSwitch.check(matches(isNotChecked())) + } + + fun assertDialogDisplayedWithTitle(title: String) { + onViewWithText(title).assertDisplayed() + } + + fun assertSyncSettingsToolbarTitle() { + onView(withText(com.instructure.student.R.string.syncSettings_toolbarTitle) + withParent(withId( + com.instructure.student.R.id.toolbar) + withAncestor(com.instructure.student.R.id.syncSettingsPage))).assertDisplayed() + } + + fun assertSyncSettingsPageDescriptions() { + onView(withText(R.string.syncSettings_autoContentSyncDescription)).assertDisplayed() + onView(withText(R.string.syncSettings_syncFrequencyDescription)).assertDisplayed() + onView(withText(R.string.syncSettings_wifiOnlyDescription)).assertDisplayed() + } + +} diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index 331d138f10..88c4ab96d8 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -95,11 +95,11 @@ import com.instructure.student.ui.pages.ShareExtensionStatusPage import com.instructure.student.ui.pages.ShareExtensionTargetPage import com.instructure.student.ui.pages.SubmissionDetailsPage import com.instructure.student.ui.pages.SyllabusPage -import com.instructure.student.ui.pages.SyncSettingsPage import com.instructure.student.ui.pages.TextSubmissionUploadPage import com.instructure.student.ui.pages.TodoPage import com.instructure.student.ui.pages.UrlSubmissionUploadPage import com.instructure.student.ui.pages.offline.ManageOfflineContentPage +import com.instructure.student.ui.pages.offline.OfflineSyncSettingsPage import com.instructure.student.ui.pages.offline.SyncProgressPage import dagger.hilt.android.testing.HiltAndroidRule import instructure.rceditor.RCETextEditor @@ -202,7 +202,7 @@ abstract class StudentTest : CanvasTest() { val importantDatesPage = ImportantDatesPage() val shareExtensionTargetPage = ShareExtensionTargetPage() val shareExtensionStatusPage = ShareExtensionStatusPage() - val syncSettingsPage = SyncSettingsPage() + val offlineSyncSettingsPage = OfflineSyncSettingsPage() val manageOfflineContentPage = ManageOfflineContentPage() val syncProgressPage = SyncProgressPage() diff --git a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt index 746dac953a..edaed771de 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/SettingsActivity.kt @@ -18,9 +18,11 @@ package com.instructure.student.activity import android.content.Context import android.content.Intent +import android.content.res.Configuration import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment +import com.instructure.interactions.FragmentInteractions import com.instructure.pandautils.analytics.SCREEN_VIEW_SETTINGS import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding @@ -55,6 +57,17 @@ class SettingsActivity : AppCompatActivity(){ private val currentFragment: Fragment? get() = supportFragmentManager.fragments.last() + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + applyThemeForAllFragments() + } + + private fun applyThemeForAllFragments() { + supportFragmentManager.fragments.forEach { + (it as? FragmentInteractions)?.applyTheme() + } + } + fun addFragment(fragment: Fragment) { val ft = supportFragmentManager.beginTransaction() currentFragment?.let { ft.hide(it) } diff --git a/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt b/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt index 5f57e39188..b3066f1c5c 100644 --- a/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt +++ b/apps/student/src/test/java/com/instructure/student/test/util/TestUtils.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.runBlocking import okhttp3.Protocol import okhttp3.Request import okhttp3.Response -import okhttp3.ResponseBody import okhttp3.ResponseBody.Companion.toResponseBody import org.hamcrest.Matcher import org.hamcrest.Matchers diff --git a/apps/teacher/build.gradle b/apps/teacher/build.gradle index 3ffda0139f..1b9fdb880c 100644 --- a/apps/teacher/build.gradle +++ b/apps/teacher/build.gradle @@ -39,8 +39,8 @@ android { defaultConfig { minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 60 - versionName = '1.26.0' + versionCode = 61 + versionName = '1.27.0' vectorDrawables.useSupportLibrary = true multiDexEnabled true testInstrumentationRunner 'com.instructure.teacher.ui.espresso.TeacherHiltTestRunner' @@ -310,6 +310,8 @@ dependencies { implementation Libs.ROOM kapt Libs.ROOM_COMPILER implementation Libs.ROOM_COROUTINES + + testImplementation Libs.HAMCREST } apply plugin: 'com.google.gms.google-services' diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt index f9ecf29cbc..4bcbed23a2 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt @@ -4,10 +4,13 @@ import android.util.Log import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import com.instructure.canvas.espresso.E2E +import com.instructure.canvas.espresso.refresh import com.instructure.dataseeding.api.ConversationsApi import com.instructure.dataseeding.api.GroupsApi import com.instructure.dataseeding.model.CanvasUserApiModel import com.instructure.dataseeding.model.CourseApiModel +import com.instructure.espresso.retry +import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory @@ -218,7 +221,11 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Select 'ARCHIVED' scope and assert that '${seedConversation2[0].subject}' conversation is displayed in the 'ARCHIVED' scope.") inboxPage.filterMessageScope("Archived") - inboxPage.assertConversationDisplayed(seedConversation2[0].subject) + + retry(times = 10, delay = 3000, block = { + refresh() + inboxPage.assertConversationDisplayed(seedConversation2[0].subject) + }) Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation and unarchive it." + "Assert that the selected number of conversation on the toolbar is 1 and '${seedConversation2[0].subject}' conversation is not displayed in the 'ARCHIVED' scope.") @@ -372,21 +379,28 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Select both of the conversations. Star them and mark the unread.") inboxPage.selectConversations(listOf(seedConversation2[0].subject, seedConversation3[0].subject)) - inboxPage.clickStar() inboxPage.clickMarkAsRead() + retry(times = 10, delay = 3000, block = { + Log.d(STEP_TAG, "Assert that '${seedConversation3[0].subject}' conversation is read.") + inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.GONE) + }) + + Log.d(STEP_TAG, "Select both of the conversations. Star them and mark the unread.") + inboxPage.clickStar() + Log.d(STEP_TAG, "Navigate to 'STARRED' scope. Assert that both of the conversation are displayed in the 'STARRED' scope.") inboxPage.filterMessageScope("Starred") - inboxPage.assertConversationDisplayed(seedConversation2[0].subject) - inboxPage.assertConversationDisplayed(seedConversation3[0].subject) + + retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) { + inboxPage.assertConversationDisplayed(seedConversation2[0].subject) + inboxPage.assertConversationDisplayed(seedConversation3[0].subject) + } Log.d(STEP_TAG, "Swipe '${seedConversation2[0].subject}' left and assert it is removed from the 'STARRED' scope because it has became unstarred.") inboxPage.swipeConversationLeft(seedConversation2[0]) inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) - Log.d(STEP_TAG, "Assert that '${seedConversation3[0].subject}' conversation is read.") - inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.GONE) - Log.d(STEP_TAG, "Swipe '${seedConversation3[0].subject}' conversation right and assert that it has became unread.") inboxPage.swipeConversationRight(seedConversation3[0].subject) inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.VISIBLE) diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt index 175815be85..cd33a35bd1 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/SpeedGraderE2ETest.kt @@ -33,12 +33,17 @@ import com.instructure.dataseeding.model.SubmissionType import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 +import com.instructure.espresso.retry import com.instructure.panda_annotations.FeatureCategory import com.instructure.panda_annotations.Priority import com.instructure.panda_annotations.TestCategory import com.instructure.panda_annotations.TestMetaData import com.instructure.teacher.R -import com.instructure.teacher.ui.utils.* +import com.instructure.teacher.ui.utils.TeacherTest +import com.instructure.teacher.ui.utils.seedAssignmentSubmission +import com.instructure.teacher.ui.utils.seedAssignments +import com.instructure.teacher.ui.utils.seedData +import com.instructure.teacher.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @@ -153,7 +158,9 @@ class SpeedGraderE2ETest : TeacherTest() { assignmentSubmissionListPage.clickFilterDialogOk() Log.d(STEP_TAG,"Assert that there is one submission displayed.") - assignmentSubmissionListPage.assertHasSubmission(1) + retry(times = 5, delay = 3000, catchBlock = { refresh() }) { + assignmentSubmissionListPage.assertHasSubmission(1) + } Log.d(STEP_TAG, "Navigate back assignment's details page.") Espresso.pressBack() diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt index c8224a11c0..5ca2eba4e2 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/LtiLaunchFragment.kt @@ -26,6 +26,8 @@ import androidx.browser.customtabs.CustomTabsIntent import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.managers.SubmissionManager import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.Tab import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.pageview.PageView @@ -108,16 +110,28 @@ class LtiLaunchFragment : BaseFragment() { var url = ltiUrl // Replace deep link scheme .replaceFirst("canvas-courses://", "${ApiPrefs.protocol}://") .replaceFirst("canvas-student://", "${ApiPrefs.protocol}://") - if (sessionLessLaunch) { - if (url.contains("api/v1/")) { - getSessionlessLtiUrl(url) - } else { - // This is specific for Studio and Gauge - url = "${ApiPrefs.fullDomain}/api/v1/accounts/self/external_tools/sessionless_launch?url=$url" + + when { + sessionLessLaunch -> { + val id = url.substringAfterLast("/external_tools/").substringBefore("?") + url = when { + (id.toIntOrNull() != null) -> when (canvasContext) { + is Course -> "${ApiPrefs.fullDomain}/api/v1/courses/${(canvasContext as Course).id}/external_tools/sessionless_launch?id=$id" + is Group -> "${ApiPrefs.fullDomain}/api/v1/groups/${(canvasContext as Group).id}/external_tools/sessionless_launch?id=$id" + else -> "${ApiPrefs.fullDomain}/api/v1/accounts/self/external_tools/sessionless_launch?id=$id" + } + + else -> { + when (canvasContext) { + is Course -> "${ApiPrefs.fullDomain}/api/v1/courses/${(canvasContext as Course).id}/external_tools/sessionless_launch?url=$url" + is Group -> "${ApiPrefs.fullDomain}/api/v1/groups/${(canvasContext as Group).id}/external_tools/sessionless_launch?url=$url" + else -> "${ApiPrefs.fullDomain}/api/v1/accounts/self/external_tools/sessionless_launch?url=$url" + } + } + } getSessionlessLtiUrl(url) } - } else { - launchCustomTab(url) + else -> launchCustomTab(url) } } else -> displayError() diff --git a/apps/teacher/src/main/java/com/instructure/teacher/view/grade_slider/PossiblePointView.kt b/apps/teacher/src/main/java/com/instructure/teacher/view/grade_slider/PossiblePointView.kt index a915a867aa..5165bf6045 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/view/grade_slider/PossiblePointView.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/view/grade_slider/PossiblePointView.kt @@ -48,11 +48,11 @@ class PossiblePointView @JvmOverloads constructor( strokeWidth = 4.toPx.toFloat() } - override fun onDraw(canvas: Canvas?) { + override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - canvas?.drawLine(anchorRect.left.toFloat(), anchorRect.bottom.toFloat() - 16.toPx, anchorRect.left.toFloat(), anchorRect.bottom.toFloat() - 8.toPx, linePaint) - canvas?.drawText(label, anchorRect.left.toFloat(), anchorRect.bottom.toFloat() + 16.toPx, textPaint) + canvas.drawLine(anchorRect.left.toFloat(), anchorRect.bottom.toFloat() - 16.toPx, anchorRect.left.toFloat(), anchorRect.bottom.toFloat() - 8.toPx, linePaint) + canvas.drawText(label, anchorRect.left.toFloat(), anchorRect.bottom.toFloat() + 16.toPx, textPaint) } fun showPossiblePoint(anchorRect: Rect, label: String) { diff --git a/apps/teacher/src/test/java/com/instructure/teacher/unit/utils/MobiusTestUtils.kt b/apps/teacher/src/test/java/com/instructure/teacher/unit/utils/MobiusTestUtils.kt index a50ba7e97b..18d223afb8 100644 --- a/apps/teacher/src/test/java/com/instructure/teacher/unit/utils/MobiusTestUtils.kt +++ b/apps/teacher/src/test/java/com/instructure/teacher/unit/utils/MobiusTestUtils.kt @@ -24,7 +24,6 @@ import com.spotify.mobius.test.NextMatchers import okhttp3.Protocol import okhttp3.Request import okhttp3.Response -import okhttp3.ResponseBody import okhttp3.ResponseBody.Companion.toResponseBody import org.hamcrest.Matcher import org.hamcrest.Matchers diff --git a/automation/espresso/build.gradle b/automation/espresso/build.gradle index 3439d3cc76..05043f647a 100644 --- a/automation/espresso/build.gradle +++ b/automation/espresso/build.gradle @@ -45,7 +45,7 @@ android { buildToolsVersion '28.0.3' defaultConfig { - minSdkVersion 17 + minSdkVersion 26 targetSdkVersion 28 } diff --git a/automation/espresso/src/main/AndroidManifest.xml b/automation/espresso/src/main/AndroidManifest.xml index 649274d1a5..a6af28ef54 100644 --- a/automation/espresso/src/main/AndroidManifest.xml +++ b/automation/espresso/src/main/AndroidManifest.xml @@ -20,6 +20,8 @@ package="com.instructure.espresso"> + + diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt index 185d7aa675..adf8d1f501 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/CanvasTest.kt @@ -21,6 +21,9 @@ import android.app.Activity import android.content.Context import android.content.res.Configuration import android.content.res.Resources +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities import android.os.Build import android.os.Environment import android.util.DisplayMetrics @@ -31,6 +34,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.matcher.ViewMatchers import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule +import androidx.test.uiautomator.UiDevice import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesCheckNames import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesViews import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult @@ -74,6 +78,8 @@ abstract class CanvasTest : InstructureTestingContract { var extraAccessibilitySupressions: Matcher? = Matchers.anyOf() + val connectivityManager = InstrumentationRegistry.getInstrumentation().context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + @Rule(order = 1) override fun chain(): TestRule { return RuleChain @@ -107,70 +113,20 @@ abstract class CanvasTest : InstructureTestingContract { // Only continue if we're on Bitrise // (More accurately, if we are on FTL launched from Bitrise.) if(splunkToken != null && !splunkToken.isEmpty()) { - val bitriseWorkflow = InstrumentationRegistry.getArguments().getString("BITRISE_TRIGGERED_WORKFLOW_ID") - val bitriseApp = InstrumentationRegistry.getArguments().getString("BITRISE_APP_TITLE") - val bitriseBranch = InstrumentationRegistry.getArguments().getString("BITRISE_GIT_BRANCH") - val bitriseBuildNumber = InstrumentationRegistry.getArguments().getString("BITRISE_BUILD_NUMBER") - - val eventObject = JSONObject() - eventObject.put("workflow", bitriseWorkflow) - eventObject.put("branch", bitriseBranch) - eventObject.put("bitriseApp", bitriseApp) - eventObject.put("status", disposition) - eventObject.put("testName", testMethod) - eventObject.put("testClass", testClass) - eventObject.put("stackTrace", error.stackTrace.take(15).joinToString(", ")) - eventObject.put("osVersion", Build.VERSION.SDK_INT.toString()) - // Limit our error message to 4096 chars; they can be unreasonably long (e.g., 137K!) when - // they contain a view hierarchy, and there is typically not much useful info after the - // first few lines. - eventObject.put("message", error.toString().take(4096)) - - val payloadObject = JSONObject() - payloadObject.put("sourcetype", "mobile-android-qa-testresult") - payloadObject.put("event", eventObject) - - val payload = payloadObject.toString() - Log.d("CanvasTest", "payload = $payload") - - // Can't run a curl command from FTL, so let's do this the hard way - var os : OutputStream? = null - var inputStream : InputStream? = null - var conn : HttpURLConnection? = null - - try { - - // Set up our url/connection - val url = URL("https://http-inputs-inst.splunkcloud.com:443/services/collector") - conn = url.openConnection() as HttpURLConnection - conn.requestMethod = "POST" - conn.setRequestProperty("Authorization", "Splunk $splunkToken") - conn.setRequestProperty("Content-Type", "application/json; utf-8") - conn.setRequestProperty("Accept", "application/json") - conn.setDoInput(true) - conn.setDoOutput(true) - - // Connect - conn.connect() - - // Send out our post body - os = BufferedOutputStream(conn.outputStream) - os.write(payload.toByteArray()) - os.flush() - - // Report the result summary - Log.d("CanvasTest", "Response code: ${conn.responseCode}, message: ${conn.responseMessage}") - - // Report the splunk result JSON - inputStream = conn.inputStream - val content = inputStream.bufferedReader().use(BufferedReader::readText) - Log.d("CanvasTest", "Response: $content") - } - finally { - // Clean up our mess - if(os != null) os.close() - if(inputStream != null) inputStream.close() - if(conn != null) conn.disconnect() + val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + val hasActiveNetwork = networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false + + if (hasActiveNetwork) { + reportToSplunk(disposition, testMethod, testClass, error, splunkToken) + } else { + turnOnConnectionViaADB() + connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + connectivityManager.unregisterNetworkCallback(this) + reportToSplunk(disposition, testMethod, testClass, error, splunkToken) + } + }) } } @@ -181,6 +137,84 @@ abstract class CanvasTest : InstructureTestingContract { } + private fun reportToSplunk( + disposition: String, + testMethod: String, + testClass: String, + error: Throwable, + splunkToken: String? + ) { + val bitriseWorkflow = + InstrumentationRegistry.getArguments().getString("BITRISE_TRIGGERED_WORKFLOW_ID") + val bitriseApp = InstrumentationRegistry.getArguments().getString("BITRISE_APP_TITLE") + val bitriseBranch = InstrumentationRegistry.getArguments().getString("BITRISE_GIT_BRANCH") + val bitriseBuildNumber = + InstrumentationRegistry.getArguments().getString("BITRISE_BUILD_NUMBER") + + val eventObject = JSONObject() + eventObject.put("workflow", bitriseWorkflow) + eventObject.put("branch", bitriseBranch) + eventObject.put("bitriseApp", bitriseApp) + eventObject.put("status", disposition) + eventObject.put("testName", testMethod) + eventObject.put("testClass", testClass) + eventObject.put("stackTrace", error.stackTrace.take(15).joinToString(", ")) + eventObject.put("osVersion", Build.VERSION.SDK_INT.toString()) + // Limit our error message to 4096 chars; they can be unreasonably long (e.g., 137K!) when + // they contain a view hierarchy, and there is typically not much useful info after the + // first few lines. + eventObject.put("message", error.toString().take(4096)) + + val payloadObject = JSONObject() + payloadObject.put("sourcetype", "mobile-android-qa-testresult") + payloadObject.put("event", eventObject) + + val payload = payloadObject.toString() + Log.d("CanvasTest", "payload = $payload") + + // Can't run a curl command from FTL, so let's do this the hard way + var os: OutputStream? = null + var inputStream: InputStream? = null + var conn: HttpURLConnection? = null + + try { + + // Set up our url/connection + val url = URL("https://http-inputs-inst.splunkcloud.com:443/services/collector") + conn = url.openConnection() as HttpURLConnection + conn.requestMethod = "POST" + conn.setRequestProperty("Authorization", "Splunk $splunkToken") + conn.setRequestProperty("Content-Type", "application/json; utf-8") + conn.setRequestProperty("Accept", "application/json") + conn.setDoInput(true) + conn.setDoOutput(true) + + // Connect + conn.connect() + + // Send out our post body + os = BufferedOutputStream(conn.outputStream) + os.write(payload.toByteArray()) + os.flush() + + // Report the result summary + Log.d( + "CanvasTest", + "Response code: ${conn.responseCode}, message: ${conn.responseMessage}" + ) + + // Report the splunk result JSON + inputStream = conn.inputStream + val content = inputStream.bufferedReader().use(BufferedReader::readText) + Log.d("CanvasTest", "Response: $content") + } finally { + // Clean up our mess + if (os != null) os.close() + if (inputStream != null) inputStream.close() + if (conn != null) conn.disconnect() + } + } + // Creates an /sdcard/coverage folder if it does not already exist. // This is necessary for us to generate/process code coverage data. private fun setupCoverageFolder() { @@ -464,6 +498,12 @@ abstract class CanvasTest : InstructureTestingContract { private var configChecked = false + private const val ENABLE_WIFI_COMMAND: String = "svc wifi enable" + private const val DISABLE_WIFI_COMMAND: String = "svc wifi disable" + + private const val ENABLE_MOBILE_DATA_COMMAND: String = "svc data enable" + private const val DISABLE_MOBILE_DATA_COMMAND: String = "svc data disable" + private fun getDeviceOrientation(context: Context): Int { val configuration = context.resources.configuration return configuration.orientation @@ -481,6 +521,19 @@ abstract class CanvasTest : InstructureTestingContract { fun isLowResDevice() : Boolean { return ApplicationProvider.getApplicationContext().resources.displayMetrics.densityDpi < DisplayMetrics.DENSITY_HIGH } + + fun turnOffConnectionViaADB() { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + device.executeShellCommand(DISABLE_WIFI_COMMAND) + device.executeShellCommand(DISABLE_MOBILE_DATA_COMMAND) + } + + fun turnOnConnectionViaADB() { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + device.executeShellCommand(ENABLE_WIFI_COMMAND) + device.executeShellCommand(ENABLE_MOBILE_DATA_COMMAND) + } + } } diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt index 254db9d454..2901820aae 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/TestingUtils.kt @@ -49,4 +49,46 @@ fun getCurrentDateInCanvasFormat(): String { val dayString = expectedDate.dayOfMonth val yearString = expectedDate.year return "$monthString $dayString, $yearString" +} + +fun retry( + times: Int = 3, + delay: Long = 1000, + catchBlock: (() -> Unit)? = null, + block: () -> Unit +) { + repeat(times - 1) { + try { + block() + return + } catch (e: Throwable) { + e.printStackTrace() + Thread.sleep(delay) + catchBlock?.invoke() + } + } + block() +} + +fun retryWithIncreasingDelay( + times: Int = 3, + initialDelay: Long = 100, + maxDelay: Long = 1000, + factor: Double = 2.0, + catchBlock: (() -> Unit)? = null, + block: () -> Unit +) { + var currentDelay = initialDelay + repeat(times - 1) { + try { + block() + return + } catch (e: Throwable) { + e.printStackTrace() + Thread.sleep(currentDelay) + currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) + catchBlock?.invoke() + } + } + block() } \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/espresso/matchers/WaitForViewMatcher.kt b/automation/espresso/src/main/kotlin/com/instructure/espresso/matchers/WaitForViewMatcher.kt index 0d543a6b25..9c56c3e5a6 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/espresso/matchers/WaitForViewMatcher.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/espresso/matchers/WaitForViewMatcher.kt @@ -51,6 +51,11 @@ object WaitForViewMatcher { return waitForViewWithCustomMatcher(viewMatcher, duration, ViewMatchers.isClickable()) } + fun waitForViewToBeCompletelyDisplayed(viewMatcher: Matcher, duration: Long = 10): ViewInteraction { + log.i("Wait for View to be completely displayed.") + return waitForViewWithCustomMatcher(viewMatcher, duration, ViewMatchers.isCompletelyDisplayed()) + } + private fun waitForViewWithCustomMatcher(viewMatcher: Matcher, duration: Long = 10, customMatcher: Matcher): ViewInteraction { waiting.set(true) val waitTime = TimeUnit.SECONDS.toMillis(duration) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index daadb630e2..d761e661c8 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation("com.android.tools.build:gradle-api:$agpVersion") implementation("org.javassist:javassist:3.24.1-GA") implementation("com.google.code.gson:gson:2.8.8") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") } plugins { diff --git a/buildSrc/src/main/java/GlobalDependencies.kt b/buildSrc/src/main/java/GlobalDependencies.kt index 4370c14f67..8ad33f36dd 100644 --- a/buildSrc/src/main/java/GlobalDependencies.kt +++ b/buildSrc/src/main/java/GlobalDependencies.kt @@ -2,21 +2,21 @@ object Versions { /* SDK Versions */ - const val COMPILE_SDK = 33 + const val COMPILE_SDK = 34 const val MIN_SDK = 26 const val TARGET_SDK = 33 /* Build/tooling */ const val ANDROID_GRADLE_TOOLS = "7.1.3" - const val BUILD_TOOLS = "30.0.3" + const val BUILD_TOOLS = "34.0.0" /* Testing */ const val JUNIT = "4.13.2" - const val ROBOLECTRIC = "4.3.1" + const val ROBOLECTRIC = "4.11.1" const val JACOCO_ANDROID = "0.1.5" /* Kotlin */ - const val KOTLIN = "1.8.10" + const val KOTLIN = "1.9.20" const val KOTLIN_COROUTINES = "1.6.4" /* Google, Play Services */ @@ -24,11 +24,11 @@ object Versions { /* Others */ const val APOLLO = "2.5.14" // There is already a brand new version, Apollo 3, that requires lots of migration - const val PSPDFKIT = "8.7.1" + const val PSPDFKIT = "8.9.1" const val PHOTO_VIEW = "2.3.0" const val MOBIUS = "1.2.1" const val SQLDELIGHT = "1.5.4" - const val HILT = "2.45" + const val HILT = "2.48" const val HILT_ANDROIDX = "1.0.0" const val LIFECYCLE = "2.6.0" const val FRAGMENT = "1.5.5" @@ -37,7 +37,8 @@ object Versions { const val RETROFIT = "2.9.0" const val OKHTTP = "4.10.0" const val HEAP = "1.10.5" - const val ROOM = "2.5.0" + const val ROOM = "2.6.0" + const val HAMCREST = "2.2" } object Libs { @@ -101,6 +102,7 @@ object Libs { /* Qr Code (zxing) */ const val JOURNEY_ZXING = "com.journeyapps:zxing-android-embedded:4.3.0" + const val JOURNEY_ZXING_CORE = "com.google.zxing:core:3.5.2" /* Dependency Inejction */ const val HILT = "com.google.dagger:hilt-android:${Versions.HILT}" @@ -157,6 +159,8 @@ object Libs { const val ROOM_COMPILER = "androidx.room:room-compiler:${Versions.ROOM}" const val ROOM_COROUTINES = "androidx.room:room-ktx:${Versions.ROOM}" const val ROOM_TEST = "androidx.room:room-testing:${Versions.ROOM}" + + const val HAMCREST = "org.hamcrest:hamcrest:${Versions.HAMCREST}" } object Plugins { diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_id.arb b/libs/flutter_student_embed/lib/l10n/res/intl_id.arb index d8ec08b159..50bfd8386a 100644 --- a/libs/flutter_student_embed/lib/l10n/res/intl_id.arb +++ b/libs/flutter_student_embed/lib/l10n/res/intl_id.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-10-28T11:03:17.232435", + "@@last_modified": "2023-08-25T11:04:30.842905", "coursesLabel": "Kursus", "@coursesLabel": { "description": "The label for the Courses tab", @@ -101,6 +101,19 @@ "points": {} } }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} acara}other{{date}, {eventCount} acara}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, "No Events Today!": "Tidak Ada Acara Hari Ini!", "@No Events Today!": { "description": "Title displayed when there are no calendar events for the current day", diff --git a/libs/login-api-2/build.gradle b/libs/login-api-2/build.gradle index 007c66956d..5df6ac4c54 100644 --- a/libs/login-api-2/build.gradle +++ b/libs/login-api-2/build.gradle @@ -116,7 +116,8 @@ dependencies { implementation Libs.ANDROIDX_RECYCLERVIEW implementation Libs.ANDROIDX_CONSTRAINT_LAYOUT - implementation Libs.JOURNEY_ZXING + implementation (Libs.JOURNEY_ZXING) { transitive = false } + implementation Libs.JOURNEY_ZXING_CORE implementation Libs.FRAGMENT_KTX implementation Libs.HILT diff --git a/libs/login-api-2/src/main/res/values-id/strings.xml b/libs/login-api-2/src/main/res/values-id/strings.xml index 7433318e75..11403151ae 100644 --- a/libs/login-api-2/src/main/res/values-id/strings.xml +++ b/libs/login-api-2/src/main/res/values-id/strings.xml @@ -110,8 +110,7 @@ school.instructure.com Hapus Pengguna Sebelumnya Tidak dapat menemukan sekolah Anda? Coba ketikkan URL sekolah lengkap. - Ketuk di sini untuk bantuan. - + Ketuk di sini untuk bantuan login. Tidak Ada Sambungan Internet Tindakan ini membutuhkan sambungan internet. Subjek dan deskripsi harus ada untuk menyerahkan umpan balik. diff --git a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt index 7b7909c6e6..e9f5f0ddb6 100644 --- a/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt +++ b/libs/panda_annotations/src/main/java/com/instructure/panda_annotations/TestMetaData.kt @@ -43,7 +43,7 @@ enum class SecondaryFeatureCategory { ASSIGNMENT_COMMENTS, ASSIGNMENT_QUIZZES, ASSIGNMENT_DISCUSSIONS, GROUPS_DASHBOARD, GROUPS_FILES, GROUPS_ANNOUNCEMENTS, GROUPS_DISCUSSIONS, GROUPS_PAGES, GROUPS_PEOPLE, EVENTS_DISCUSSIONS, EVENTS_QUIZZES, EVENTS_ASSIGNMENTS, EVENTS_NOTIFICATIONS, - MODULES_ASSIGNMENTS, MODULES_DISCUSSIONS, MODULES_FILES, MODULES_PAGES, MODULES_QUIZZES + MODULES_ASSIGNMENTS, MODULES_DISCUSSIONS, MODULES_FILES, MODULES_PAGES, MODULES_QUIZZES, OFFLINE_MODE } enum class TestCategory { diff --git a/libs/pandares/src/main/res/values-ar/strings.xml b/libs/pandares/src/main/res/values-ar/strings.xml index d99ab6d62a..d2f6ca711d 100644 --- a/libs/pandares/src/main/res/values-ar/strings.xml +++ b/libs/pandares/src/main/res/values-ar/strings.xml @@ -1481,6 +1481,21 @@ تحديد الكل إلغاء تحديد الكل حدث خطأ أثناء تحميل المحتوى. + لا يوجد أي مساق + سيتم إدراج مساقاتك هنا، ثم يمكنك إتاحته للاستخدام دون اتصال. + لا يوجد محتوى مساق + سيتم إدراج محتوى المساق هنا، ثم يمكنك إتاحته للاستخدام دون اتصال. + هل تريد تجاهل التغييرات؟ + إذا اخترت التجاهل، لن يتم حفظ التغييرات. + تجاهل + هل تريد مزامنة المحتوى دون اتصال؟ + سيؤدي هذا إلى مزامنة ~%s من المحتوى. وقد يؤدي هذا إلى رسوم إضافية من موفر الخدمة إذا لم تكن متصلاً بشبكة Wi-Fi. + سيؤدي هذا إلى مزامنة ~%s من المحتوى فقط أثناء اتصالك بشبكة Wi-Fi. + مزامنة + جارٍ التحميل... + مهلاً، نحن نجهز لك الأشياء. + توسيع المحتوى + طي المحتوى سيؤدي تمكين مزامنة المحتوى التلقائية إلى تنزيل المحتوى المحدد بناءً على الإعدادات أدناه. ستحدث مزامنة المحتوى حتى لو لم يكن يعمل التطبيق. إذا تم إيقاف تشغيل الإعداد، فلن تحدث المزامنة. لن يتم حذف المحتوى الذي تم تنزيله بالفعل. تكرار المزامنة مزامنة المحتوى التلقائية @@ -1527,7 +1542,6 @@ %d من المساقات قيد المزامنة. %d من المساقات قيد المزامنة. - صور محتوى المساق هذه المهمة لم تعد متوفرة. أنت غير متصل بالإنترنت ليس لديك حاليًا أي مساقات غير متوفرة عبر الإنترنت. @@ -1543,4 +1557,5 @@ تمت مزامنة %d من المساقات. تمت مزامنة %d من المساقات. + محتوى مساق إضافي diff --git a/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml b/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml index 692494e0bb..9dfdafe588 100644 --- a/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+da+DK+instk12/strings.xml @@ -1410,6 +1410,21 @@ Vælg alle Fravælg alle Der opstod en fejl under indlæsning af indholdet. + Ingen fag + Dine fag vil blive vist her, og derefter kan du gøre dem tilgængelige til offline brug. + Intet fagindhold + Fagindholdet vil blive vist her, og derefter kan du gøre det tilgængeligt til offline brug. + Annuller ændringer? + Hvis du vælger at kassere, vil ændringerne ikke blive gemt. + Kasser + Vil du synkronisere offline indhold? + Dette vil synkronisere ~%s indhold. Det kan medføre yderligere gebyrer fra din dataudbyder, hvis du ikke er forbundet til et wi-fi-netværk. + Dette vil kun synkronisere ~%s indhold, mens du er tilsluttet et wi-fi-netværk. + Synkroniser + Indlæser ... + Vent venligst, mens vi gør tingene klar til dig. + Udvid indhold + Skjul indholdet Aktivering af automatisk indholdssynkronisering sørger for at downloade det valgte indhold baseret på nedenstående indstillinger. Indholdssynkroniseringen gennemføres, selvom applikationen ikke kører. Hvis indstillingen er deaktiveret, sker der ingen synkronisering. Det allerede downloadede indhold vil ikke blive slettet. Synkroniseringsfrekvens Automatisk indholdssynkronisering @@ -1452,7 +1467,6 @@ %d faget synkroniseres. %d fag synkroniseres. - Fagindhold billeder Denne opgave er ikke længere tilgængelig. Du er offline Du har i øjeblikket ingen fag, der er tilgængelige offline. @@ -1464,4 +1478,5 @@ %d faget er blevet synkroniseret. %d fagene er blevet synkroniseret. + Yderligere fagindhold diff --git a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml index c2f7ac0a04..fe5fecc1f2 100644 --- a/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+AU+unimelb/strings.xml @@ -1410,6 +1410,21 @@ Select All Deselect All An error occurred while loading the content. + No Subjects + Your subjects will be listed here, and then you can make them available for offline usage. + No Subject Content + The subject content will be listed here, and then you can make them available for offline usage. + Discard Changes? + If you choose to discard, the changes will not be saved. + Discard + Sync Offline Content? + This will sync ~%s content. It may result in additional charges from your data provider if you are not connected to a Wi-Fi network. + This will sync ~%s content only while you are connected to a Wi-Fi network. + Sync + Loading... + Hang tight, we\'re getting things ready for you. + Expand content + Collapse content Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronisation will happen even if the application is not running. If the setting is switched off, then no synchronisation will happen. The already downloaded content will not be deleted. Sync Frequency Auto Content Sync @@ -1452,7 +1467,6 @@ %d subject is syncing. %d subjects are syncing. - Subject content images This assignment is no longer available. You are offline You currently don\'t have any subjects that are available offline. @@ -1464,4 +1478,5 @@ %d subject has been synced. %d subjects have been synced. + Additional subject content diff --git a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml index 9a7a75afe0..a86cf9a071 100644 --- a/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml +++ b/libs/pandares/src/main/res/values-b+en+GB+instukhe/strings.xml @@ -1410,6 +1410,21 @@ Select all Un-select All An error occurred while loading the content. + No Modules + Your modules will be listed here, and then you can make them available for offline usage. + No Module Content + The module content will be listed here, and then you can make them available for offline usage. + Discard Changes? + If you choose to discard, the changes will not be saved. + Discard + Sync Offline Content? + This will sync ~%s content. It may result in additional charges from your data provider, if you are not connected to a Wi-Fi network. + This will sync ~%s content only while you are connected to a Wi-Fi network. + Sync + Loading... + Hang tight, we\'re getting things ready for you. + Expand content + Collapse content Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronization will happen even if the application is not running. If the setting is switched off, then no synchronization will happen. The already downloaded content will not be deleted. Sync Frequency Auto Content Sync @@ -1452,7 +1467,6 @@ %d module is syncing. %d modules are syncing. - Module content images This assignment is no longer available. You are offline You currently don\'t have any modules that are available offline. @@ -1464,4 +1478,5 @@ %d module has been synced. %d modules have been synced. + Additional module content diff --git a/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml b/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml index 90a7760b69..b48a88b2e6 100644 --- a/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+nb+NO+instk12/strings.xml @@ -1411,6 +1411,21 @@ Velg alle Fjern all merking Det oppsto en feil ved lasting av innholdet. + Ingen fag + Fagene dine vil vises her, og du kan deretter gjøre det tilgjengelig for frakoblet bruk. + Intet faginnhold + Faginnholdet vises her, og du kan deretter gjøre det tilgjengelig for frakoblet bruk. + Forkaste endringer? + Hvis du velger å forkaste, blir ikke endringene lagret. + Forkast + Synkronisere frakoblet innhold? + Dette vil synkronisere ~%s-innhold. Dette kan føre til ekstra kostnader fra din datatilbyder hvis du ikke er koblet til et Wi-Fi-nettverk. + Dette vil synkronisere ~%s-innhold bare når du er tilkoblet et Wi-Fi-nettverk. + Synkroniser + Laster… + Vent litt, vi gjør alt klart for deg. + Utvid innhold + Skjul innhold Aktivering av Automatisk synkronisering av innhold vil ta seg av nedlasting av det valgte innholdet basert på følgende innstillinger. Synkronisering av innhold vil skje selv om applikasjonen ikke kjører. Hvis innstillingen er slått av, vil det ikke skje noen synkronisering. Det allerede nedlastede innholdet vil ikke bli slettet. Synkroniseringsfrekvens Automatisk synkronisering av innhold @@ -1453,7 +1468,6 @@ %d fag synkroniseres. %d fag synkroniseres. - Faginnhold-bilder Denne oppgaven er ikke lenger tilgjengelig. Du er frakoblet Du har ingen fag som er tilgjengelig i frakoblet modus. @@ -1465,4 +1479,5 @@ %d fag er synkronisert. %d fag er synkronisert. + Ytterligere faginnhold diff --git a/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml b/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml index 63545c2531..5d2ad46623 100644 --- a/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml +++ b/libs/pandares/src/main/res/values-b+sv+SE+instk12/strings.xml @@ -1410,6 +1410,21 @@ Välj alla Avmarkera alla Ett fel uppstod vid inläsning av innehållet. + Inga kurser + Dina kurser kommer att listas här, och sedan kan du göra dem tillgängliga för offlineanvändning. + Inget kursinnehåll + Kursinnehållet kommer att listas här, och sedan kan du göra dem tillgängliga för offlineanvändning. + Ta bort ändringar? + Om du väljer att avvisa kommer ändringarna inte att sparas. + Avbryt + Synkronisera offlineinnehåll? + Detta synkroniserar ~%s-innehåll. Detta kan medföra ytterligare kostnader från din dataleverantör om du inte är ansluten till ett wifi-nätverk. + Detta synkroniserar ~%s-innehåll endast när du är ansluten till ett wifi-nätverk. + Synkronisera + Läser in ... + Vänta, vi förbereder åt dig. + Visa innehållet + Dölj innehållet Om automatisk innehållssynkronisering aktiveras laddas det valda innehållet ned baserat på nedanstående inställningar. Innehållssynkroniseringen sker även om programmet inte körs. Om inställningen är avstängd sker ingen synkronisering. Innehåll som redan laddats ned raderas inte. Synkroniseringsfrekvens Automatisk innehållssynkronisering @@ -1452,7 +1467,6 @@ %d-kurs synkroniserar. %d-kurser synkroniserar. - Bilder i kursinnehållet Denna uppgift är inte längre tillgänglig. Du är offline Du har för närvarande inte några kurser som är tillgängliga offline. @@ -1464,4 +1478,5 @@ %d-kurs har synkroniserats. %d-kurser har synkroniserats. + Extra kursinnehåll diff --git a/libs/pandares/src/main/res/values-b+zh+HK/strings.xml b/libs/pandares/src/main/res/values-b+zh+HK/strings.xml index 1c0a7022e7..c38571beeb 100644 --- a/libs/pandares/src/main/res/values-b+zh+HK/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+HK/strings.xml @@ -1392,6 +1392,21 @@ 全選 取消全選 載入內容時發生錯誤。 + 無課程 + 您的課程將將在此處列出,然後您可以讓它們可以離線使用。 + 沒有課程內容 + 課程內容將在此處列出,然後您可以讓它們可以離線使用。 + 捨棄變更? + 如果您選擇捨棄,系統將不會儲存變更。 + 放棄 + 同步離線內容? + 這將同步 ~%s 內容。如果您未連線到 Wi-Fi 網路,則可能會導致您的資料提供者收取額外費用。 + 這將僅在您連線到 Wi-Fi 網路時同步 ~%s 內容。 + 同步 + 正在載入…… + 請稍等,我們已經為您準備好了。 + 展開內容 + 收起內容 啟用「自動內容同步」將根據以下設定注意下載所選取的內容。內容同步將會進行,即使應用程式並未正在執行中。如果關閉設定,則將不會進行同步。已經下載的內容也將不會刪除。 同步頻率 自動內容同步 @@ -1433,7 +1448,6 @@ %d 課程正在同步中。 - 課程內容影像 此作業不再可用。 您已離線 您目前沒有任何可離線使用的課程。 @@ -1444,4 +1458,5 @@ %d 課程已同步。 + 額外課程內容 diff --git a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml index f9e4a98069..d2e4fdcb21 100644 --- a/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+Hans/strings.xml @@ -1392,6 +1392,21 @@ 全选 取消全选 加载内容时出错。 + 没有课程 + 此处将列出您的课程,然后您可以使内容离线可用。 + 无课程内容 + 此处将列出课程内容,然后您可以使内容离线可用。 + 放弃更改? + 如果您选择放弃,更改将不会保存。 + 放弃 + 同步离线内容? + 大约会有 %s 项内容同步。如果未连接到无线网络,您的数据流量提供商可能会额外收费。 + 仅当连接到无线网络时,大约会有 %s 项内容同步。 + 同步 + 加载中... + 正在准备中,请稍等。 + 扩展内容 + 折叠内容 如果启用“自动同步内容”,将根据以下设置下载所选内容。即使应用程序没有运行,内容也会同步。如果关闭该设置,将不会同步。已下载的内容不会被删除。 同步周期 自动同步内容 @@ -1433,7 +1448,6 @@ %d 门课程正在同步。 - 课程内容图像 此作业不再可用。 您已离线 您目前没有任何可离线使用的课程。 @@ -1444,4 +1458,5 @@ %d 门课程已同步。 + 更多课程内容 diff --git a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml index 1c0a7022e7..c38571beeb 100644 --- a/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml +++ b/libs/pandares/src/main/res/values-b+zh+Hant/strings.xml @@ -1392,6 +1392,21 @@ 全選 取消全選 載入內容時發生錯誤。 + 無課程 + 您的課程將將在此處列出,然後您可以讓它們可以離線使用。 + 沒有課程內容 + 課程內容將在此處列出,然後您可以讓它們可以離線使用。 + 捨棄變更? + 如果您選擇捨棄,系統將不會儲存變更。 + 放棄 + 同步離線內容? + 這將同步 ~%s 內容。如果您未連線到 Wi-Fi 網路,則可能會導致您的資料提供者收取額外費用。 + 這將僅在您連線到 Wi-Fi 網路時同步 ~%s 內容。 + 同步 + 正在載入…… + 請稍等,我們已經為您準備好了。 + 展開內容 + 收起內容 啟用「自動內容同步」將根據以下設定注意下載所選取的內容。內容同步將會進行,即使應用程式並未正在執行中。如果關閉設定,則將不會進行同步。已經下載的內容也將不會刪除。 同步頻率 自動內容同步 @@ -1433,7 +1448,6 @@ %d 課程正在同步中。 - 課程內容影像 此作業不再可用。 您已離線 您目前沒有任何可離線使用的課程。 @@ -1444,4 +1458,5 @@ %d 課程已同步。 + 額外課程內容 diff --git a/libs/pandares/src/main/res/values-ca/strings.xml b/libs/pandares/src/main/res/values-ca/strings.xml index bcb976cfa4..9cec769a5d 100644 --- a/libs/pandares/src/main/res/values-ca/strings.xml +++ b/libs/pandares/src/main/res/values-ca/strings.xml @@ -1411,6 +1411,21 @@ Selecciona-ho tot Anul·la la selecció de tot S\'ha produït un error en carregar el contingut. + No hi ha cap assignatura + Les vostres assignatures s’enumeraran aquí; posteriorment, podeu fer que estiguin disponibles per utilitzar-se sense connexió. + No hi ha cap contingut de l’assignatura + El contingut de l’assignatura s’enumerarà aquí; posteriorment, podeu fer que estigui disponible per utilitzar-se sense connexió. + Voleu rebutjar els canvis? + Si trieu rebutjar-los, no es desaran els canvis. + Rebutja + Voleu sincronitzar el contingut sense connexió? + Amb aquesta operació se sincronitzarà el contingut de ~%s. Si no us heu connectat a cap xarxa Wi-Fi, és possible que el proveïdor de dades us cobri algun import addicional. + Amb aquesta operació se sincronitzarà el contingut de ~%s només mentre tingueu connexió a una xarxa Wi-Fi. + Sincronitza + S\'està carregant… + Un moment, us ho estem preparant. + Desplega el contingut + Contrau el contingut En activar la sincronització automàtica del contingut es baixarà el contingut seleccionat segons les opcions de configuració indicades a continuació. Se sincronitzarà el contingut encara que l’aplicació no s’estigui executant. Si el paràmetre està desactivat no es durà a terme la sincronització. No se suprimirà el contingut que ja s’hagi baixat. Freqüència de sincronització Sincronització automàtica del contingut @@ -1453,7 +1468,6 @@ S’està sincronitzant %d assignatura. S’estan sincronitzant %d assignatures. - Imatges del contingut de l\'assignatura Aquesta activitat ja no està disponible. Esteu sense connexió Actualment, no teniu cap assignatura que estigui disponible sense connexió. @@ -1465,4 +1479,5 @@ S’ha sincronitzat %d assignatura. S’han sincronitzat %d assignatures. + Contingut de l’assignatura addicional diff --git a/libs/pandares/src/main/res/values-cy/strings.xml b/libs/pandares/src/main/res/values-cy/strings.xml index 84e4e1f225..64444749c2 100644 --- a/libs/pandares/src/main/res/values-cy/strings.xml +++ b/libs/pandares/src/main/res/values-cy/strings.xml @@ -1410,6 +1410,21 @@ Dewis y cyfan Dad-ddewis y Cyfan Gwall wrth lwytho’r cynnwys. + Dim Cyrsiau + Bydd eich cyrsiau’n cael eu rhestru yma, ac yna gallwch chi ei gwneud ar gael i\'w defnyddio all-lein. + Dim Cynnwys Cwrs + Bydd cynnwys y cwrs yn cael ei restru yma, ac yna gallwch chi ei gwneud ar gael i\'w defnyddio all-lein. + Hepgor y Newidiadau? + Os byddwch chi’n dewis hepgor, ni fydd y newidiadau’n cael eu cadw. + Hepgor + Cysoni Cynnwys All-lein? + Bydd hyn yn cysoni ~%s cynnwys. Gall hyn arwain at gostau ychwanegol gan eich darparwr data, os nad ydych chi wedi cysylltu â rhwydwaith Wi-Fi. + Bydd hyn yn cysoni ~%s cynnwys dim ond tra rydych chi wedi eich cysylltu i rwydwaith Wi-Fi. + Cysoni + Wrthi’n llwytho... + Arhoswch, rydym ni’n cael pethau’n barod i chi. + Ehangu cynnwys + Crebachu cynnwys Bydd galluogi Cysoni Cynnwys Awtomatig yn gofalu am lwytho’r cynnwys sydd wedi’i ddewis i lawr yn seiliedig ar y gosodiadau isod. Bydd cysoni cynnwys yn digwydd hyd yn oed os nad yw’r rhaglen yn rhedeg. Os yw’r gosodiadau wedi’i ddiffodd ni fydd cysoni’n digwydd. Ni fydd cynnwys sydd eisoes wedi’i lwytho i lawr yn cael ei ddileu. Amlder Cysoni Cysoni Cynnwys Awtomatig @@ -1452,7 +1467,6 @@ %d cwrs yn cysoni. %d cwrs yn cysoni. - Delweddau cynnwys cwrs Dydy’r aseiniad hwn ddim ar gael mwyach. Rydych chi all-lein Ar hyn o bryd, does gennych chi ddim cyrsiau sydd ar gael all-lein. @@ -1464,4 +1478,5 @@ %d cwrs wedi cael ei gysoni. %d cwrs wedi cael eu cysoni. + Cynnwys cwrs ychwanegol diff --git a/libs/pandares/src/main/res/values-da/strings.xml b/libs/pandares/src/main/res/values-da/strings.xml index 9823813992..c51f15be4f 100644 --- a/libs/pandares/src/main/res/values-da/strings.xml +++ b/libs/pandares/src/main/res/values-da/strings.xml @@ -1410,6 +1410,21 @@ Vælg alle Fravælg alle Der opstod en fejl under indlæsning af indholdet. + Ingen kurser + Dine fag vil blive vist her, og derefter kan du gøre dem tilgængelige til offline brug. + Intet fagindhold + Fagindholdet vil blive vist her, og derefter kan du gøre det tilgængeligt til offline brug. + Annuller ændringer? + Hvis du vælger at kassere, vil ændringerne ikke blive gemt. + Kasser + Vil du synkronisere offline indhold? + Dette vil synkronisere ~%s indhold. Det kan medføre yderligere gebyrer fra din dataudbyder, hvis du ikke er forbundet til et wi-fi-netværk. + Dette vil kun synkronisere ~%s indhold, mens du er tilsluttet et wi-fi-netværk. + Synkroniser + Indlæser ... + Vent venligst, mens vi gør tingene klar til dig. + Udvid indhold + Skjul indholdet Aktivering af automatisk indholdssynkronisering sørger for at downloade det valgte indhold baseret på nedenstående indstillinger. Indholdssynkroniseringen gennemføres, selvom applikationen ikke kører. Hvis indstillingen er deaktiveret, sker der ingen synkronisering. Det allerede downloadede indhold vil ikke blive slettet. Synkroniseringsfrekvens Automatisk indholdssynkronisering @@ -1452,7 +1467,6 @@ %d faget synkroniseres. %d fag synkroniseres. - Fagindhold billeder Denne opgave er ikke længere tilgængelig. Du er offline Du har i øjeblikket ingen fag, der er tilgængelige offline. @@ -1464,4 +1478,5 @@ %d faget er blevet synkroniseret. %d fagene er blevet synkroniseret. + Yderligere fagindhold diff --git a/libs/pandares/src/main/res/values-de/strings.xml b/libs/pandares/src/main/res/values-de/strings.xml index 820eb976ff..da151b2dc7 100644 --- a/libs/pandares/src/main/res/values-de/strings.xml +++ b/libs/pandares/src/main/res/values-de/strings.xml @@ -1410,6 +1410,21 @@ Alle auswählen Alle abwählen Beim Laden des Inhalts ist ein Fehler aufgetreten. + Keine Kurse + Ihre Kurse werden hier aufgelistet und können dann für die Offline-Nutzung verfügbar gemacht werden. + Keine Kursinhalte + Die Kursinhalte werden hier aufgelistet und können dann für die Offline-Nutzung verfügbar gemacht werden. + Änderungen verwerfen? + Wenn Sie die Änderungen verwerfen, werden sie nicht gespeichert. + Verwerfen + Offline-Inhalte synchronisieren + Damit werden ~%s Inhalte synchronisiert. Es können zusätzliche Gebühren von Ihrem Datenanbieter anfallen, wenn Sie nicht mit einem WLAN-Netzwerk verbunden sind. + Damit werden ~%s Inhalte synchronisiert, wenn Sie mit einem WLAN-Netzwerk verbunden sind. + Synchronisieren + Wird geladen ... + Bleiben Sie dran, wir bereiten alles für Sie vor. + Content erweitern + Content ausblenden Wenn Sie die automatische Inhaltssynchronisierung aktivieren, wird der Download der ausgewählten Inhalte auf der Grundlage der unten aufgeführten Einstellungen durchgeführt. Die Synchronisierung der Inhalte erfolgt auch dann, wenn die Anwendung nicht ausgeführt wird. Wenn die Einstellung ausgeschaltet ist, findet keine Synchronisierung statt. Die bereits heruntergeladenen Inhalte werden nicht gelöscht. Synchronisierungsfrequenz Automatische Synchronisation von Inhalten @@ -1452,7 +1467,6 @@ %d Kurs wird synchronisiert. %d Kurse werden synchronisiert. - Bilder zum Kursinhalt Diese Aufgabe ist nicht mehr verfügbar. Sie sind offline Sie haben derzeit keine offline verfügbaren Kurse. @@ -1464,4 +1478,5 @@ %d Kurs wurde synchronisiert. %d Kurse wurden synchronisiert. + Zusätzliche Kursinhalte diff --git a/libs/pandares/src/main/res/values-en-rAU/strings.xml b/libs/pandares/src/main/res/values-en-rAU/strings.xml index a94e856389..ce9cf16f72 100644 --- a/libs/pandares/src/main/res/values-en-rAU/strings.xml +++ b/libs/pandares/src/main/res/values-en-rAU/strings.xml @@ -1410,6 +1410,21 @@ Select All Deselect All An error occurred while loading the content. + No Courses + Your courses will be listed here, and then you can make them available for offline usage. + No Course Content + The course content will be listed here, and then you can make them available for offline usage. + Discard Changes? + If you choose to discard, the changes will not be saved. + Discard + Sync Offline Content? + This will sync ~%s content. It may result in additional charges from your data provider if you are not connected to a Wi-Fi network. + This will sync ~%s content only while you are connected to a Wi-Fi network. + Sync + Loading... + Hang tight, we\'re getting things ready for you. + Expand content + Collapse content Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronisation will happen even if the application is not running. If the setting is switched off, then no synchronisation will happen. The already downloaded content will not be deleted. Sync Frequency Auto Content Sync @@ -1452,7 +1467,6 @@ %d course is syncing. %d courses are syncing. - Course content images This assignment is no longer available. You are offline You currently don\'t have any courses that are available offline. @@ -1464,4 +1478,5 @@ %d course has been synced. %d courses have been synced. + Additional course content diff --git a/libs/pandares/src/main/res/values-en-rCY/strings.xml b/libs/pandares/src/main/res/values-en-rCY/strings.xml index 9a7a75afe0..a86cf9a071 100644 --- a/libs/pandares/src/main/res/values-en-rCY/strings.xml +++ b/libs/pandares/src/main/res/values-en-rCY/strings.xml @@ -1410,6 +1410,21 @@ Select all Un-select All An error occurred while loading the content. + No Modules + Your modules will be listed here, and then you can make them available for offline usage. + No Module Content + The module content will be listed here, and then you can make them available for offline usage. + Discard Changes? + If you choose to discard, the changes will not be saved. + Discard + Sync Offline Content? + This will sync ~%s content. It may result in additional charges from your data provider, if you are not connected to a Wi-Fi network. + This will sync ~%s content only while you are connected to a Wi-Fi network. + Sync + Loading... + Hang tight, we\'re getting things ready for you. + Expand content + Collapse content Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronization will happen even if the application is not running. If the setting is switched off, then no synchronization will happen. The already downloaded content will not be deleted. Sync Frequency Auto Content Sync @@ -1452,7 +1467,6 @@ %d module is syncing. %d modules are syncing. - Module content images This assignment is no longer available. You are offline You currently don\'t have any modules that are available offline. @@ -1464,4 +1478,5 @@ %d module has been synced. %d modules have been synced. + Additional module content diff --git a/libs/pandares/src/main/res/values-en-rGB/strings.xml b/libs/pandares/src/main/res/values-en-rGB/strings.xml index a838cfd2cd..f714fcd87a 100644 --- a/libs/pandares/src/main/res/values-en-rGB/strings.xml +++ b/libs/pandares/src/main/res/values-en-rGB/strings.xml @@ -1410,6 +1410,21 @@ Select all Un-select All An error occurred while loading the content. + No Courses + Your courses will be listed here, and then you can make them available for offline usage. + No Course Content + The course content will be listed here, and then you can make them available for offline usage. + Discard Changes? + If you choose to discard, the changes will not be saved. + Discard + Sync Offline Content? + This will sync ~%s content. It may result in additional charges from your data provider, if you are not connected to a Wi-Fi network. + This will sync ~%s content only while you are connected to a Wi-Fi network. + Sync + Loading... + Hang tight, we\'re getting things ready for you. + Expand content + Collapse content Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronization will happen even if the application is not running. If the setting is switched off, then no synchronization will happen. The already downloaded content will not be deleted. Sync Frequency Auto Content Sync @@ -1452,7 +1467,6 @@ %d course is syncing. %d courses are syncing. - Course content images This assignment is no longer available. You are offline You currently don\'t have any courses that are available offline. @@ -1464,4 +1478,5 @@ %d course has been synced. %d courses have been synced. + Additional course content diff --git a/libs/pandares/src/main/res/values-es-rES/strings.xml b/libs/pandares/src/main/res/values-es-rES/strings.xml index 37d94a6525..a7ceba25e1 100644 --- a/libs/pandares/src/main/res/values-es-rES/strings.xml +++ b/libs/pandares/src/main/res/values-es-rES/strings.xml @@ -1412,6 +1412,21 @@ Seleccionar todo Deseleccionar todo Ha habido un error al cargar el contenido. + No hay cursos + Tus cursos aparecerán aquí y luego, podrás habilitarlos para utilizarlos sin conexión. + No hay contenido del curso + El contenido del curso aparecerá aquí y luego, podrás habilitarlo para utilizarlo sin conexión. + ¿Descartar los cambios? + Si optas por descartar, los cambios no se guardarán. + Descartar + ¿Sincronizar contenido sin conexión? + Esto sincronizará ~%s contenido. Si no estás conectado a una red wifi, es posible que tu proveedor de datos cobre cargos adicionales. + Esto sincronizará ~%s contenido, pero solo mientras estés conectado a una red wifi. + Sincronización + Cargando... + Ponte cómodo, estamos preparando todo para ti. + Expandir contenido + Colapsar contenido Con la acción de habilitar la sincronización automática de contenidos, se descargará el contenido seleccionado según los siguientes ajustes. La sincronización del contenido se realizará incluso aunque la aplicación no esté en funcionamiento. Si los ajustes están apagados, no se realizará la sincronización. El contenido ya descargado no podrá eliminarse. Frecuencia de sincronización Sincronización automática del contenido @@ -1454,7 +1469,6 @@ %d curso se está sincronizando. %d cursos se están sincronizando. - Contenido de las imágenes del curso Esta actividad ya no está disponible. No tienes conexión Actualmente no tienes ningún curso disponible sin conexión. @@ -1466,4 +1480,5 @@ %d curso se ha sincronizado. %d cursos se han sincronizado. + Contenido del curso adicional diff --git a/libs/pandares/src/main/res/values-es/strings.xml b/libs/pandares/src/main/res/values-es/strings.xml index 71d1cdce1e..380810ec03 100644 --- a/libs/pandares/src/main/res/values-es/strings.xml +++ b/libs/pandares/src/main/res/values-es/strings.xml @@ -1410,6 +1410,21 @@ Seleccionar todo Desmarcar todo Hubo un error al cargar el contenido. + Sin cursos + Sus cursos se mostrarán aquí y luego podrá ponerlos a disposición para su uso sin conexión. + Sin contenido del curso + El contenido del curso se mostrará aquí y luego podrá ponerlo a disposición para su uso sin conexión. + ¿Descartar los cambios? + Si elige descartarlos, no se guardarán los cambios. + Descartar + ¿Quiere sincronizar contenido sin conexión? + Esto sincronizará ~%s de contenido. Si no se conecta a una red Wi-Fi, es posible que su proveedor de datos le aplique cargos adicionales. + Esto sincronizará ~%s de contenido solo mientras tenga conexión a una red Wi-Fi. + Sincronización + Cargando... + Aguarde un momento. Estamos preparando todo para usted. + Expandir contenido + Contraer contenido Habilitar la sincronización automática de contenido se ocupará de descargar el contenido seleccionado en función de las configuraciones siguientes. La sincronización de contenido se realizará incluso si la aplicación no se está ejecutando. Si la configuración está desactivada, no se realizará ninguna sincronización. El contenido ya descargado no se eliminará. Frecuencia de sincronización Sincronización automática de contenido @@ -1452,7 +1467,6 @@ Se está sincronizando el curso %d. Se están sincronizando los cursos %d. - Imágenes del contenido del curso Esta tarea ya no está disponible. Está sin conexión Actualmente, no tiene ningún curso disponible sin conexión. @@ -1464,4 +1478,5 @@ Se ha sincronizado el curso %d. Se han sincronizado los cursos %d. + Contenido adicional del curso diff --git a/libs/pandares/src/main/res/values-fi/strings.xml b/libs/pandares/src/main/res/values-fi/strings.xml index 2e783d8563..b5cbe900ab 100644 --- a/libs/pandares/src/main/res/values-fi/strings.xml +++ b/libs/pandares/src/main/res/values-fi/strings.xml @@ -1410,6 +1410,21 @@ Valitse kaikki Poista kaikkien valinta Kurssin sisältöä ladattaessa ilmeni virhe. + Ei kursseja + Kurssisi näkyvät täällä, ja voit sitten asettaa sen saataville verkon ulkopuoliseen käyttöön. + Ei kurssisisältöä + Kurssisältö näkyy täällä, ja voit sitten asettaa sen saataville verkon ulkopuoliseen käyttöön. + Ohitetaanko muutokset? + Jos valitset ohittavasi, muutoksia ei tallenneta. + Ohita + Synkronoidaanko verkon ulkopuolinen sisältö + Tämä synkronisoi ~%s sisällön. Tästä saattaa aiheutua ylimääräisiä veloituksia datan toimittajilta, jos et ole yhteydessä Wi-Fi-verkkoon. + Tämä synkronisoi ~%s sisällön ainoastaan silloin, kn yhteys Wi-Fi -verkkoon on saatavilla. + Synkronointi + Ladataan... + Pysyttele täällä, valmistamme täällä kaikkea sinulle. + Laajenna sisältöä. + Kutista sisältöä. Automaattisen sisällön synkronisoinnin ottamisellal käyttöön huolehditaan valitun sisällön latauksesta alla olevien asetusten perusteella. Sisällön synkronisointi tapahtuu myös silloin, kun sovellus ei ole käynnissä jos asetukset on kytketty pois päältä, synkronisointia ei tapahdu. Jo ladattua sisältöä ei poisteta. Synkronoinnin tiheys Automaattinen sisällön synkronisointi @@ -1452,7 +1467,6 @@ %d kurssia synkronoidaan. %d kurssia synkronoidaan. - Kurssin sisällön kuvakkeet. Tämä tehtävä ei enää ole käytettävissä. Olet verkon ulkopuolella Sinulla ei parhaillaan ole kursseja, jotka ovat saatavilla verkon ulkopuolella. @@ -1464,4 +1478,5 @@ %d kurssi on synkronoitu. %d kurssia on synkronoitu. + LIsäkurssin sisältö. diff --git a/libs/pandares/src/main/res/values-fr-rCA/strings.xml b/libs/pandares/src/main/res/values-fr-rCA/strings.xml index 72c52b2f56..0de87d4836 100644 --- a/libs/pandares/src/main/res/values-fr-rCA/strings.xml +++ b/libs/pandares/src/main/res/values-fr-rCA/strings.xml @@ -1410,6 +1410,21 @@ Sélectionner tout Désélectionner tout Une erreur s’est produite lors du chargement du contenu. + Aucun cours + Vos cours seront répertoriés ici, puis vous pourrez les rendre disponibles pour une utilisation hors ligne. + Pas de contenu de cours + Le contenu du cours sera répertorié ici, puis vous pourrez le rendre disponible pour une utilisation hors ligne. + Abandonner les modifications? + Si vous choisissez d’annuler, les modifications ne seront pas enregistrées. + Abandonner + Synchronisation du contenu hors connexion? + Cela synchronisera le contenu de ~%s. Cela peut entraîner des frais supplémentaires de la part de votre fournisseur de données, si vous n’êtes pas connecté à un réseau Wi-Fi. + Cela synchronisera le contenu de ~%s uniquement lorsque vous êtes connecté à un réseau Wi-Fi. + Synchronisation + En cours de chargement... + Tenez bon, on prépare les choses pour vous. + Développer le contenu + Réduire le contenu L’activation de la synchronisation automatique du contenu se chargera de télécharger le contenu sélectionné en fonction des paramètres ci-dessous. La synchronisation du contenu se produit même si l’application n’est pas en cours d’exécution. Si le paramètre est désactivé, aucune synchronisation ne se produira. Le contenu déjà téléchargé ne sera pas supprimé. Fréquence de synchronisation Synchronisation automatique du contenu @@ -1452,7 +1467,6 @@ Synchronisation de %d cours. Synchronisation de %d cours. - Images du contenu du cours Cette tâche n’est plus disponible. Vous êtes hors ligne Vous n’avez actuellement aucun cours disponible hors ligne. @@ -1464,4 +1478,5 @@ %d cours a été synchronisé. %d cours ont été synchronisés. + Contenu supplémentaire de cours diff --git a/libs/pandares/src/main/res/values-fr/strings.xml b/libs/pandares/src/main/res/values-fr/strings.xml index b4f1328b77..21add8a7e4 100644 --- a/libs/pandares/src/main/res/values-fr/strings.xml +++ b/libs/pandares/src/main/res/values-fr/strings.xml @@ -1410,6 +1410,21 @@ Tout sélectionner Tout désélectionner Une erreur est survenue lors du chargement du contenu. + Aucun cours + Vos cours seront répertoriés ici et vous pourrez les rendre disponibles pour une utilisation hors ligne. + Aucun contenu de cours + Le contenu du cours sera répertorié ici et vous pourrez le rendre disponible pour une utilisation hors ligne. + Abandonner les modifications ? + Si vous choisissez Abandonner, les modifications ne seront pas enregistrées. + Abandonner + Synchroniser le contenu hors ligne ? + Le contenu ~%s sera synchronisé. Votre fournisseur de données peut vous facturer des frais supplémentaires si vous n’êtes pas connecté à un réseau Wi-Fi. + Le contenu ~%s ne sera synchronisé que lorsque vous serez connecté à un réseau Wi-Fi. + Synchroniser + Chargement en cours... + Un petit instant, nous sommes en train de tout préparer. + Développer le contenu + Réduire le contenu L\'activation de la synchronisation auto du contenu gérera automatiquement le téléchargement du contenu sélectionné en fonction des paramètres ci-dessous. La synchronisation du contenu aura lieu même si l’application n’est pas en cours d’exécution. Si le paramètre est désactivé, aucune synchronisation n’aura lieu. Le contenu déjà téléchargé ne sera pas supprimé. Fréquence de la synchronisation Synchronisation auto du contenu @@ -1452,7 +1467,6 @@ %d cours est en cours de synchronisation. %d cours sont en cours de synchronisation. - Images du contenu du cours Ce travail n’est plus disponible. Vous êtes hors ligne. Vous n’avez actuellement aucun cours disponible hors ligne. @@ -1464,4 +1478,5 @@ %d cours a été synchronisé. %d cours ont été synchronisés. + Contenu de cours supplémentaire diff --git a/libs/pandares/src/main/res/values-ht/strings.xml b/libs/pandares/src/main/res/values-ht/strings.xml index 5eda272465..02d547258d 100644 --- a/libs/pandares/src/main/res/values-ht/strings.xml +++ b/libs/pandares/src/main/res/values-ht/strings.xml @@ -1410,6 +1410,21 @@ Seleksyone Tout Deseleksyone Tout Yon erè fèt pandan chajman kontni an. + Pa gen Kou + Kou ou yo ap afiche isit la, apre sa ou ka mete yo disponib pou itilize san koneksyon. + Pa gen Kontni Kou + Kontni kou a ap afiche isit la, apre sa ou ka mete yo disponib pou itilize san koneksyon. + Rejte Chanjman? + si w chwazi rejte yo, chanjman yo pa p sovgade. + Abandone + Senkwonize Kontni San Koneksyon? + Sa ap senkwonize ~%s kontni. Li ka lakoz founisè sèvis entènèt ou a touche anplis si w pa konekte sou yon rezo wi-fi. + Sa ap senkwonize ~%s kontni sèlman lè w konekte sou yon rezo wi-fi. + Sync + Chajman... + Talè, n\ ap prepare bagay yo pou ou. + Elaji kontni + Ratresi kontni Aktivasyon senkwonizasyon Kontni Otomatik la ap gen pou telechaje kontni ki seleksyone a an fonksyon de paramèt ki anba yo. Senkwonizasyon kontni an ap fèt menm si aplikasyon an pa ekzekite. Si reglaj la dezaktive, pa gen senkwonizasyon k ap fèt. Kontni ki deja telechaje a pa p efase. Frekans Senkwonizasyon Senkwonizasyon Kontni Otomatik @@ -1452,7 +1467,6 @@ %d kou an senkwonizasyon %d kou yo ap senkwonize. - Imaj kontni kou Travay sa pa diponib ankò. Ou pa konekte Kounye a ou pa gen okenn kou ki disponib san koneksyon. @@ -1464,4 +1478,5 @@ %d kou a enkwonize. %d kou yo senkwonize. + Plis kontni kou diff --git a/libs/pandares/src/main/res/values-id/strings.xml b/libs/pandares/src/main/res/values-id/strings.xml index bbaf905458..bbe108efcb 100644 --- a/libs/pandares/src/main/res/values-id/strings.xml +++ b/libs/pandares/src/main/res/values-id/strings.xml @@ -432,6 +432,7 @@ Dihapus + Nilai diperbarui Memuat Konten Canvas… UnknownDevice @@ -1116,6 +1117,8 @@ %s %s %s %s, %s + Anda dapat membuka detail Penyerahan dari sini + Nilai: %s %s Menit %s Menit @@ -1299,10 +1302,18 @@ Tutup dialog kemajuan %1$s dari %2$s Mengunggah ke File + Satu atau lebih file gagal diunggah. Periksa sambungan internet Anda dan coba serahkan lagi. Mengunggah penyerahan ke \"%s\" Mengunggah Penyerahan - Mengunggah File + Penyerahan Berhasil + Penyerahan Gagal + + Mengunggah File + Mengunggah File + + Unggahan File Berhasil + Unggahan File Gagal Batalkan Penyerahan Ini akan membatalkan dan menghapus penyerahan Anda. Unggahan File Gagal @@ -1310,7 +1321,9 @@ Unggah ke File Saya Serahkan tugas Pilih kursus + Pilih kursus, kursus yang dipilih adalah: %s Pilih tugas + Pilih tugas, tugas yang dipilih adalah: %s %1$s, %2$s Batalkan Unggahan? Ini akan membatalkan unggahan Anda. @@ -1325,4 +1338,148 @@ Dibuat oleh Student View Kesalahan terjadi. Topik mungkin tidak lagi tersedia. Izin kamera ditolak secara permanen. Buka pengaturan app untuk mengizinkannya. + Tidak dapat menerbitkan tugas jika ada penyerahan siswa. + Berlangganan ke Umpan Kalender + Anda dapat menyinkronkan kalender Canvas ke akun Google Calendar Anda dengan mengeklik tombol Subscribe di dialog ini. Lalu, Anda harus pergi ke aplikasi Google Calendar di perangkat Anda dan mengaktifkan sinkronisasi di pengaturan untuk kalender baru. + Berlangganan + Alihkan ke Mode Terang + Alihkan ke Mode Gelap + Anda pengguna baru atau Kebijakan Penggunaan yang Dapat Diterima telah berubah sejak Anda terakhir kali menyetujuinya. Silakan setujui Kebijakan Penggunaan yang Dapat Diterima sebelum Anda melanjutkan. + Kebijakan Penggunaan yang Dapat Diterima + Kebijakan Penggunaan yang Dapat Diterima + Saya menyetujui Kebijakan Penggunaan yang Dapat Diterima. + Serahkan + Gagal menyerahkan penerimaan terhadap ketentuan. + Upaya %d + Pertanyaan: %d + Batas waktu: %s + Upaya yang diizinkan: %s + Upaya digunakan: %d + Terjadi kesalahan yang tidak terduga. + Hapus + Arsipkan + Keluarkan dari arsip + Tandai sudah dibaca + Tandai belum dibaca + Bintang + Hapus Bintang + Gagal melakukan operasi + %s dihapus + %s diarsipkan + %s dikeluarkan dari arsip + %s ditandai sudah dibaca + %s ditandai belum dibaca + %s dibintangi + %s bintang dihapus + + Percakapan ini akan dihapus dari Kotak Masuk di semua perangkat Anda. Tindakan ini tidak bisa diurungkan. + Percakapan ini akan dihapus dari Kotak Masuk di semua perangkat Anda. Tindakan ini tidak bisa diurungkan. + + Gagal memuat halaman selanjutnya. Periksa sambungan internet Anda. + Gagal memuat ulang percakapan. Periksa sambungan internet Anda. + Kotak Masuk + Pilih Kursus atau Grup + Kosongkan + Filter Kursus: %s + pilih + Percakapan dipilih. Mode seleksi diaktifkan. Navigasikan untuk tindakan. + Percakapan dipilih + Percakapan batal dipilih + Keluar mode seleksi + Mode seleksi dinonaktifkan. + Avatar dari %s + Urungkan + App + Domain + ID Login + Email + Versi + Ada masalah saat memuat ulang tugas ini. Silakan periksa sambungan internet Anda dan coba lagi. + Logo Instructure + Preferensi + Konten Offline + Sinkronisasi + + Konten Offline + Kelola Konten Offline + Penyimpanan + %s dari %s Digunakan + Aplikasi Lain + Canvas Student + Tersisa + Semua Kursus + Sinkronkan + %d Dipilih + Pilih Semua + Hapus pilihan semua + Kesalahan terjadi saat memuat konten. + Tidak Ada Kursus + Kursus Anda akan tertera di sini, lalu Anda dapat menyediakannya untuk penggunaan offline. + Tidak Ada Konten Kursus + Konten kursus akan tertera di sini, lalu Anda dapat menyediakannya untuk penggunaan offline. + Buang Perubahan? + Jika Anda memutuskan untuk membuang, perubahan tidak akan disimpan. + Buang + Sinkronisasikan Konten Offline? + Ini akan menyinkronkan konten ~%s. Ini dapat menghasilkan biaya tambahan dari operator seluler Anda jika Anda tidak tersambung ke jaringan Wi-Fi. + Ini hanya akan menyinkronkan konten ~%s ketika Anda tersambung ke jaringan Wi-Fi. + Sinkronkan + Memuat... + Jangan ke mana-mana, kami sedang menyiapkannya untuk Anda. + Buka konten + Tutup konten + Mengaktifkan Sinkronisasi Konten Otomatis akan menangani pengunduhan dari konten yang dipilih berdasarkan pengaturan di bawah. Sinkronisasi konten akan terjadi walaupun aplikasi tidak berjalan. Jika pengaturan dimatikan, sinkronisasi tidak akan terjadi. Konten yang sudah diunduh tidak akan dihapus. + Frekuensi Sinkronisasi + Sinkronisasi Konten Otomatis + Spesifikasikan kemunculan ulang sinkronisasi konten. Sistem akan mengunduh konten yang dipilih berdasarkan frekuensi yang ditetapkan di sini. + Hanya Sinkronisasikan Konten Melalui Wi-Fi + Jika pengaturan ini diaktifkan, sinkronisasi konten hanya akan terjadi jika perangkat tersambung ke jaringan Wi-Fi; jika tidak, akan ditunda hingga jaringan Wi-Fi tersedia. + Sinkronisasi + Setiap Hari + Mingguan + Frekuensi Sinkronisasi + Matikan Sinkronisasikan Konten Hanya Melalui Wi-Fi? + Jika pengaturan ini diaktifkan, sinkronisasi konten hanya akan terjadi jika perangkat tersambung ke jaringan Wi-Fi; jika tidak, akan ditunda hingga jaringan Wi-Fi tersedia. + Matikan + Manual + Mode Offline + Tidak Tersedia Offline + Konten ini tidak tersedia dalam mode offline. + Konten ini tidak tersedia dalam mode offline. Jika Anda ingin mengubah pengaturan, buka layar Konten Offline dari dashboard ketika jaringan tersedia. + Offline + Sinkronisasi Gagal + Mengunduh %1$s dari %2$s + Diantrekan + Sinkronisasi Konten Offline Selesai + Sinkronisasi Konten Offline Gagal + Batalkan Sinkronisasi? + Akan menghentikan sinkronisasi konten offline. Anda dapat melakukannya lagi nanti. + Satu atau lebih file gagal disinkronkan. Periksa sambungan internet Anda dan coba serahkan lagi. + Unduhan dimulai + Kursus tidak dapat ditambahkan ke offline favorit. + Semua Kursus + Kursus + Grup + Semua Kursus + Memilih kursus untuk Dashboard hanya dapat dilakukan offline. Anda dapat menavigasikan ke detail kursus offline. + Catatan + Sukses! Mengunduh %1$s dari %2$s + Menyinkronisasikan Konten Offline? + Abaikan notifikasi + + Kursus %d disinkronkan. + Kursus %d disinkronkan. + + Tugas tidak lagi tersedia. + Anda offline + Anda saat ini tidak memiliki kursus apa pun yang tersedia offline. + Sinkronisasi Konten Offline Berhasil + Sinkronisasi Konten Offline Gagal + Pembaruan Sinkronisasi Offline + Notifikasi Canvas untuk pembaruan sinkronisasi offline. + + Kursus %d telah disinkronkan. + Kursus %d telah disinkronkan. + + Konten kursus tambahan diff --git a/libs/pandares/src/main/res/values-is/strings.xml b/libs/pandares/src/main/res/values-is/strings.xml index 05e63a199b..19cda25d85 100644 --- a/libs/pandares/src/main/res/values-is/strings.xml +++ b/libs/pandares/src/main/res/values-is/strings.xml @@ -1410,6 +1410,21 @@ Velja allt Afvelja allt Villa kom fram við að hlaða innihaldið. + Engin námskeið + Námskeiðin þín verða skráð hér og síðan geturðu gert þau aðgengileg til notkunar án nettengingar. + Ekkert námskeiðsefni + Efni námskeiðsins verður skráð hér og síðan geturðu gert það aðgengilegt til notkunar án nettengingar. + Henda breytingum? + Breytingarnar verða ekki vistaðar ef þú velur að henda. + Henda + Samhæfa efni án nettengingar? + Þetta mun samhæfa ~%s efni. Það getur leitt til viðbótargjalda frá gagnaveitunni þinni ef þú ert ekki tengd(ur) við Wi-Fi net. + Þetta mun eingöngu samhæfa ~%s efni á meðan þú ert tengd(ur) við Wi-Fi net. + Samhæfa + Hleður... + Bíddu við, við erum að gera hlutina tilbúna fyrir þig. + Víkka efni + Fella saman efni Með því að virkja sjálfvirka samstillingu efnis mun hún sjá um að hlaða niður völdu efni byggt á stillingunum hér að neðan. Samstilling efnis mun gerast jafnvel þótt forritið sé ekki í gangi. Ef slökkt er á stillingunni mun engin samstilling eiga sér stað. Efni sem þegar hefur verið hlaðið niður verður ekki eytt. Samstillingartíðni Sjálfvirk samstilling efnis @@ -1452,7 +1467,6 @@ %d námskeið er að samstillast. %d námskeið eru að samstillast. - Myndir af innihaldi námskeiðs Þetta verkefni er ekki lengur tiltækt. Þú ert án nettengingar Þú ert ekki með nein námskeið sem eru í boði án nettengingar. @@ -1464,4 +1478,5 @@ %d námskeið hefur verið samstillt. %d námskeið hafa verið samstillt. + Viðbótar námskeiðsefni diff --git a/libs/pandares/src/main/res/values-it/strings.xml b/libs/pandares/src/main/res/values-it/strings.xml index 1ddabbeb86..b01288d3eb 100644 --- a/libs/pandares/src/main/res/values-it/strings.xml +++ b/libs/pandares/src/main/res/values-it/strings.xml @@ -1410,6 +1410,21 @@ Seleziona tutto Deseleziona tutto Si è verificato un errore durante il caricamento dei contenuti. + Nessun corso + I tuoi corsi saranno elencati qui, quindi è possibile renderli disponibili per l’uso offline. + Nessun contenuto del corso + Il contenuto del corso sarà elencato qui, quindi è possibile renderlo disponibile per l’uso offline. + Annullare le modifiche? + Se scegli di eliminarle, le modifiche non saranno salvate. + Rimuovi + Sincronizzare contenuto offline? + Sarà sincronizzato il contenuto ~%s. Ciò può comportare l’addebito di costi aggiuntivi da parte del tuo fornitore di dati, nel caso in cui tu non sia collegato a una rete Wi-Fi. + Sarà sincronizzato il contenuto ~%s solo quando sei collegato ad una rete Wi-Fi. + Sincronizza + Caricamento in corso... + Un attimo di pazienza, stiamo preparando dei contenuti per te. + Espandi contenuti + Comprimi contenuti L’attivazione della Sincronizzazione contenuto automatica si occuperà del download del contenuto selezionato in base alle impostazioni riportate di seguito. La sincronizzazione del contenuto verrà effettuata anche se l’applicazione non è in funzione. Se l’impostazione è disattiva, non sarà effettuata alcuna sincronizzazione. Il contenuto già scaricato non sarà eliminato. Frequenza di sincronizzazione Sincronizzazione contenuto automatica @@ -1452,7 +1467,6 @@ %d corso in sincronizzazione. %d corsi in sincronizzazione. - Immagini contenuto corso Questo compito non è più disponibile. Sei offline Al momento non hai nessun corso disponibile offline. @@ -1464,4 +1478,5 @@ %d corso è stato sincronizzato. %d corsi sono stati sincronizzati. + Altro contenuto del corso diff --git a/libs/pandares/src/main/res/values-ja/strings.xml b/libs/pandares/src/main/res/values-ja/strings.xml index a64094ca34..2bd6db565b 100644 --- a/libs/pandares/src/main/res/values-ja/strings.xml +++ b/libs/pandares/src/main/res/values-ja/strings.xml @@ -1392,6 +1392,21 @@ すべて選択 すべての選択を取り消し コンテンツ読み込み中にエラーが起こりました。 + コースはありません + コースがここに表示され、オフラインで利用できるようにすることができます。 + コースコンテンツなし + コースコンテンツはここに表示され、オフラインで利用できるようになります。 + 変更を破棄しますか? + 破棄を選択した場合、変更は保存されません。 + 破棄する + オフラインのコンテンツを同期化する + これで、~%sのコンテンツが同期されます。Wi-Fiネットワークに接続していない、場合、データプロバイダから追加料金が請求される可能性があります。 + これで、Wi-Fiネットワークに接続している間のみ、~%sのコンテンツが同期されます + 同期化 + 読み込み中・・・ + 少々お待ちください。今、準備をしています。 + コンテンツを拡大 + コンテンツを折りたたむ コンテンツの自動同期を有効にすると、選択したコンテンツのダウンロードが以下の設定に基づいて行われます。コンテンツの同期は、アプリケーションが起動していなくても行われます。この設定をオフにすると、同期は行われません。すでにダウンロードされているコンテンツは削除されません。 同期の頻度 コンテンツ自動同期 @@ -1433,7 +1448,6 @@ %dコースは同期中です。 - コースコンテンツ画像 この機能はもう利用できません。 現在オフラインです 現在、オフラインで利用可能なコースはありません。 @@ -1444,4 +1458,5 @@ %dコースが同期されました。 + 追加のコースコンテンツ diff --git a/libs/pandares/src/main/res/values-mi/strings.xml b/libs/pandares/src/main/res/values-mi/strings.xml index 0dbbc8f33e..0121ebff4d 100644 --- a/libs/pandares/src/main/res/values-mi/strings.xml +++ b/libs/pandares/src/main/res/values-mi/strings.xml @@ -1410,6 +1410,21 @@ Tīpako katoa Whakakorehia te katoa I puta he hapa i te wa e uta ana te ihirangi. + Kāore he Akoranga + Ka whakarārangihia ō akoranga ki reira ka taea e koe te whakawātea mō te whakamahi tuimotu. + Karekau he ihirangi akoranga + Kua whakarārangitia te ihirangi akoranga ki konei katahi ka taea e koe te waatea mo te whakamahi tuimotu. + Tūraki ngā Huringa? + Mena ka whiriwhiri koe ki te whakakore, ka kore nga huringa e tiakina. + Tūraki + Tukutahi ihirangi tuimotu? + Tenei ka tukutahi ~%s ihirangi. Ka hua pea he utu taapiri mai i to kaiwhakarato raraunga, mena kaore koe e hono ki te whatunga Wi-Fi. + Ma tenei ka tukutahi ~%s ihirangi anake i te wa e hono ana koe ki te whatunga Wi-Fi. + Tukutahi + E uta ana .... + Kia mau, tatou kei te whakarite mea mo koe. + Roha ihirangi + Tiango ihirangi Ina whakahohea te waahanga Tukutahi Ihirangi Aunoa, ko nga ihirangi kua tikiakehia ka whakawhirinaki ki nga tautuhinga kua whakarārangihia i raro nei. Ahakoa kaore i tuwhera te tono, ka mau tonu te tukutahitanga ihirangi. Karekau he tukutahitanga mena kua weto te tautuhinga. Ko nga mea kua oti te tango ake ka kore e tangohia. Auautanga Tukutahi Tukutahi Ihirangi Aunoa @@ -1452,7 +1467,6 @@ %d Kei te tukutahi te akoranga. %d kei te tukutahi nga akoranga. - Whakaahua ihirangi akoranga Kaore tēnei whakataunga i te wātea. Kei te tuimotu koe I tenei wa karekau he akoranga kei te waatea tuimotu. @@ -1464,4 +1478,5 @@ %d kua tukutahia te akoranga. %d kua tukutahia nga akoranga. + Ihirangi akoranga taapiri diff --git a/libs/pandares/src/main/res/values-ms/strings.xml b/libs/pandares/src/main/res/values-ms/strings.xml index 7848518aa7..85e960c5fd 100644 --- a/libs/pandares/src/main/res/values-ms/strings.xml +++ b/libs/pandares/src/main/res/values-ms/strings.xml @@ -1416,6 +1416,21 @@ Pilih Semua Nyahpilih Semua Ralat berlaku semasa memuatkan kandungan. + Tiada Kursus + kursus anda akan disenaraikan di sini, kemudian anda boleh menyediakannya untuk kegunaan luar talian. + Tiada Kandungan Kursus + Kandungan kursus akan disenaraikan di sini, kemudian anda boleh menyediakannya untuk kegunaan luar talian. + Buang Perubahan? + Jika anda memilih untuk membuangnya, perubahan tidak akan disimpan. + Buang + Segerakkan Kandungan Luar Talian? + Tindakan ini akan menyegerakkan ~%s kandungan. Hal ini mungkin akan menyebabkan anda dikenakan caj tambahan oleh penyedia data anda sekiranya anda tidak bersambung ke rangkaian Wi-Fi. + Tindakan ini akan menyegerakkan ~%s kandungan hanya ketika anda bersambung ke rangkaian Wi-Fi. + Segerakkan + Memuatkan... + Sila tunggu, kami sedang menyediakan beberapa perkara untuk anda. + Kembangkan kandungan + Kuncupkan kandungan Mendayakan Segerakan Kandungan Automatik akan mengurus muat turun kandungan yang dipilih berdasarkan tetapan di bawah. Penyegerakan kandungan akan etap berlaku walaupun aplikasi tidak berjalan. Jika tetapan dimatikan maka tiada penyegerakan akan berlaku. Kandungan yang sudah dimuat turun tidak akan dipadamkan. Kekerapan Penyegerakan Segerakan Kandungan Automatik @@ -1458,7 +1473,6 @@ %d Kursus sedang disegerakkan. %d Kursus sedang disegerakkan. - Imej kandungan kursus Tugasan ini tidak lagi tersedia. Anda berada di luar talian Anda tidak mempunyai apa-apa kursus yang tersedia di luar talian buat masa ini. @@ -1470,4 +1484,5 @@ %d Kursus telah disegerakkan. %d Kursus telah disegerakkan. + Kandungan kursus tambahan diff --git a/libs/pandares/src/main/res/values-nb/strings.xml b/libs/pandares/src/main/res/values-nb/strings.xml index 81e7b55592..436fbd2300 100644 --- a/libs/pandares/src/main/res/values-nb/strings.xml +++ b/libs/pandares/src/main/res/values-nb/strings.xml @@ -1411,6 +1411,21 @@ Velg alle Fjern all merking Det oppsto en feil ved lasting av innholdet. + Ingen emner + Emnene dine vil vises her, og du kan deretter gjøre det tilgjengelig for frakoblet bruk. + Intet emneinnhold + Emneinnholdet vises her, og du kan deretter gjøre det tilgjengelig for frakoblet bruk. + Forkaste endringer? + Hvis du velger å forkaste, blir ikke endringene lagret. + Forkast + Synkronisere frakoblet innhold? + Dette vil synkronisere ~%s-innhold. Dette kan føre til ekstra kostnader fra din datatilbyder hvis du ikke er koblet til et Wi-Fi-nettverk. + Dette vil synkronisere ~%s-innhold bare når du er tilkoblet et Wi-Fi-nettverk. + Synkroniser + Laster… + Vent litt, vi gjør alt klart for deg. + Utvid innhold + Skjul innhold Aktivering av Automatisk synkronisering av innhold vil ta seg av nedlasting av det valgte innholdet basert på følgende innstillinger. Synkronisering av innhold vil skje selv om applikasjonen ikke kjører. Hvis innstillingen er slått av, vil det ikke skje noen synkronisering. Det allerede nedlastede innholdet vil ikke bli slettet. Synkroniseringsfrekvens Automatisk synkronisering av innhold @@ -1453,7 +1468,6 @@ %d emne synkroniseres. %d emner synkroniseres. - Emneinnhold-bilder Denne oppgaven er ikke lenger tilgjengelig. Du er frakoblet Du har ingen emner som er tilgjengelig i frakoblet modus. @@ -1465,4 +1479,5 @@ %d emne er synkronisert. %d emner er synkronisert. + Ytterligere emneinnhold diff --git a/libs/pandares/src/main/res/values-nl/strings.xml b/libs/pandares/src/main/res/values-nl/strings.xml index 182e1a3bac..ac1db111a8 100644 --- a/libs/pandares/src/main/res/values-nl/strings.xml +++ b/libs/pandares/src/main/res/values-nl/strings.xml @@ -1410,6 +1410,21 @@ Alles selecteren Selectie van alle items opheffen Er is een fout opgetreden tijdens het laden van de content. + Geen cursussen + Je cursussen worden hier weergegeven, waarvan je de inhoud beschikbaar kunt maken voor offline gebruik. + Geen cursusinhoud + De cursusinhoud wordt hier weergegeven, waar je deze beschikbaar kunt maken voor offline gebruik. + Wijzigingen negeren? + Als je kiest voor verwijderen, worden de wijzigingen niet opgeslagen. + Verwijderen + Offline inhoud synchroniseren? + Hiermee wordt ~%s inhoud gesynchroniseerd. Dat kan leiden tot extra kosten van je dataprovider als je niet verbonden bent met een wifi-netwerk. + Hiermee wordt ~%s inhoud alleen gesynchroniseerd als je verbonden bent met een wifi-netwerk. + Synchroniseren + Bezig met laden... + Een ogenblikje, we zetten het voor je klaar. + Inhoud uitvouwen + Inhoud samenvouwen Door Automatisch synchroniseren van content in te schakelen wordt de geselecteerde content gedownload op basis van de onderstaande instellingen. De contentsynchronisatie vindt ook plaats als de applicatie niet wordt uitgevoerd. Als de instelling wordt uitgeschakeld, wordt er geen synchronisatie uitgevoerd. De reeds gedownloade content wordt niet verwijderd. Synchronisatiefrequentie Automatisch synchroniseren van content @@ -1452,7 +1467,6 @@ %d-cursus wordt gesynchroniseerd. %d-cursussen worden gesynchroniseerd. - Afbeeldingen van cursusinhoud Deze opdracht is niet meer beschikbaar. Je bent offline Je hebt momenteel geen cursussen die offline beschikbaar zijn. @@ -1464,4 +1478,5 @@ %d-cursus is gesynchroniseerd. %d-cursussen zijn gesynchroniseerd. + Aanvullende cursusinhoud diff --git a/libs/pandares/src/main/res/values-pl/strings.xml b/libs/pandares/src/main/res/values-pl/strings.xml index 2a0f55ce2f..78c4a5ce2a 100644 --- a/libs/pandares/src/main/res/values-pl/strings.xml +++ b/libs/pandares/src/main/res/values-pl/strings.xml @@ -1446,6 +1446,21 @@ Zaznacz wszystko Usuń zaznaczenie wszystkich Podczas wczytywania zawartości wystąpił błąd. + Brak kursów + Kursy zostaną wyszczególnione tutaj, następnie można je będzie udostępnić do użytku offline. + Brak zawartości kursu + Zawartość kursu zostanie podana tutaj, następnie można ją będzie udostępnić do użytku offline. + Odrzucić zmiany? + W przypadku odrzucenia, zmiany nie zostaną zapisane. + Odrzuć + Zsynchronizować zawartość offline? + Spowoduje to synchronizację zawartości ~%s. Może to skutkować pobraniem dodatkowych opłat przez usługodawcę, jeśli nie nawiązano połączenia z siecią Wi-Fi. + Spowoduje to synchronizację zawartości ~%s, jeśli nawiązano połączenie z siecią Wi-Fi. + Synchronizacja + Wczytywanie... + Chwileczkę, wszystko będzie niedługo gotowe. + Rozwiń zawartość + Zwiń zawartość Włączenie automatycznej synchronizacji zawartości pozwoli pobierać wybraną zawartość w oparciu o poniższe ustawienia. Synchronizacja zawartości będzie się odbywać, nawet jeśli aplikacja nie zostanie włączona. Jeśli funkcja jest wyłączona, synchronizacja nie będzie działać. Pobrana już zawartość nie zostanie usunięta. Częstotliwość synchronizacji Automatyczna synchronizacja zawartości @@ -1490,7 +1505,6 @@ Trwa synchronizacja kursów %d. Trwa synchronizacja kursów %d. - Obrazy zawartości kursu To zadanie nie jest już dostępne. Jesteś offline Obecnie nie masz żadnych kursów dostępnych offline. @@ -1504,4 +1518,5 @@ Kursy %d zostały zsynchronizowane. Kursy %d zostały zsynchronizowane. + Dodatkowa zawartość kursu diff --git a/libs/pandares/src/main/res/values-pt-rBR/strings.xml b/libs/pandares/src/main/res/values-pt-rBR/strings.xml index 743fe896ee..31760bd583 100644 --- a/libs/pandares/src/main/res/values-pt-rBR/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rBR/strings.xml @@ -1410,6 +1410,21 @@ Selecionar tudo Cancelar seleção de todos Ocorreu um erro ao carregar o conteúdo. + Sem Cursos + Seus cursos serão listados aqui e você poderá disponibilizá-los para uso offline. + Nenhum conteúdo do curso + O conteúdo do curso será listado aqui e você poderá disponibilizá-lo para uso offline. + Descartar alterações? + Se você optar por descartar, as alterações não serão salvas. + Descartar + Sincronizar conteúdo off-line? + Isso sincronizará o conteúdo ~%s. Isso pode resultar em cobranças adicionais do seu provedor de dados, se você não estiver conectado a uma rede Wi-Fi. + Isso sincronizará o conteúdo ~%s apenas enquanto você estiver conectado a uma rede Wi-Fi. + Sincronizar + Carregando... + Aguarde, estamos preparando tudo para você. + Expandir conteúdo + Recolher conteúdo A ativação da sincronização automática de conteúdo cuidará do download do conteúdo selecionado com base nas configurações abaixo. A sincronização de conteúdo acontecerá mesmo se o aplicativo não estiver em execução. Se a configuração estiver desativada, nenhuma sincronização ocorrerá. O conteúdo já baixado não será excluído. Frequência de sincronização Sincronização automática de conteúdo @@ -1452,7 +1467,6 @@ %d curso está sendo sincronizado. %d cursos estão sendo sincronizados. - Imagens do conteúdo do curso Essa tarefa não está mais disponível. Você está off-line No momento, você não tem nenhum curso disponível off-line. @@ -1464,4 +1478,5 @@ %d curso foi sincronizado. %d cursos foram sincronizados. + Conteúdo adicional do curso diff --git a/libs/pandares/src/main/res/values-pt-rPT/strings.xml b/libs/pandares/src/main/res/values-pt-rPT/strings.xml index 0e9fe3c275..4a06a3eafc 100644 --- a/libs/pandares/src/main/res/values-pt-rPT/strings.xml +++ b/libs/pandares/src/main/res/values-pt-rPT/strings.xml @@ -1410,6 +1410,21 @@ Selecionar tudo Desmarcar todos Ocorreu um erro ao carregar o conteúdo. + Sem Disciplinas + As suas disciplinas serão listadas aqui e, em seguida, pode disponibilizá-las para utilização offline. + Sem conteúdo da disciplina + Os conteúdos da disciplina serão listados aqui e, em seguida, pode disponibilizá-los para utilização offline. + Ignorar mudanças? + Se optar por rejeitar, as alterações não serão guardadas. + Ignorar + Sincronizar conteúdo offline? + Isto irá sincronizar o conteúdo ~%s. Pode resultar em custos adicionais do seu fornecedor de dados, se não estiver ligado a uma rede Wi-Fi. + Esta opção sincronizará o conteúdo ~%s apenas quando estiver ligado a uma rede Wi-Fi. + Sincronizar + A carregar... + Aguente firme, estamos a preparar tudo para si. + Expandir conteúdo + Recolher conteúdo A ativação da Sincronização automática de conteúdos encarregar-se-á de descarregar o conteúdo selecionado com base nas definições abaixo. A sincronização de conteúdos ocorrerá mesmo que a aplicação não esteja a ser executada. Se a definição estiver desativada, não será efetuada qualquer sincronização. O conteúdo já transferido não será eliminado. Frequência de sincronização Sincronização automática de conteúdos @@ -1452,7 +1467,6 @@ %d a disciplina está a sincronizar-se. %d as disciplinas estão a sincronizar-se. - Imagens do conteúdo da disciplina Esta tarefa não está mais disponível. Está offline Atualmente, não tem quaisquer disciplinas disponíveis offline. @@ -1464,4 +1478,5 @@ %d a disciplina foi sincronizada. %d as disciplinas foram sincronizadas. + Conteúdo adicional da disciplina diff --git a/libs/pandares/src/main/res/values-ru/strings.xml b/libs/pandares/src/main/res/values-ru/strings.xml index 5130909e65..b4d4c66796 100644 --- a/libs/pandares/src/main/res/values-ru/strings.xml +++ b/libs/pandares/src/main/res/values-ru/strings.xml @@ -1446,6 +1446,21 @@ Выбрать все Отменить выбор для всех Произошла ошибка при загрузке контента. + Курсы отсутствуют + Ваши курсы будут перечислены здесь, а затем вы сможете сделать их доступными для использования в режиме офлайн. + Нет контента курса + Содержание курсов будет перечислено здесь, а затем вы сможете сделать их доступными для использования в режиме офлайн. + Отменить изменения? + Если выбрать отмену, изменения не будут сохранены. + Удалить + Синхронизировать офлайн-контент? + Будет выполнена синхронизация содержимого ~%s. Это может привести к дополнительным расходам со стороны поставщика данных, если вы не подключены к сети Wi-Fi. + При этом синхронизация контента ~%s будет осуществляться только при подключении к сети Wi-Fi. + Синхронизировать + Выполняется загрузка... + Спокойно, мы готовим все для вас. + Развернуть содержание + Свернуть содержание При включении функции автоматической синхронизации содержимого будет выполнена загрузка выбранного содержимого на основе приведенных ниже настроек. Синхронизация содержимого будет происходить, даже если приложение не запущено. Если этот параметр выключен, синхронизация не будет выполняться. Уже загруженное содержимое не будет удаляться. Синхронизация частоты Автоматическая синхронизация контента @@ -1490,7 +1505,6 @@ %d курса(-ов) синхронизируются. %d курса(-ов) синхронизируются. - Изображения содержимого курса Это задание более недоступно. Вы находитесь в автономном режиме В настоящее время у вас нет никаких курсов, доступных в автономном режиме. @@ -1504,4 +1518,5 @@ %d курса(-ов) были синхронизированы. %d курса(-ов) были синхронизированы. + Дополнительный контент курса diff --git a/libs/pandares/src/main/res/values-sl/strings.xml b/libs/pandares/src/main/res/values-sl/strings.xml index c5e6bbde77..8bacdc881f 100644 --- a/libs/pandares/src/main/res/values-sl/strings.xml +++ b/libs/pandares/src/main/res/values-sl/strings.xml @@ -1410,6 +1410,21 @@ Izberi vse Razveljavi izbor vseh Pri nalaganju vsebine je prišlo do napake. + Ni predmetov + Vaši predmeti bodo navedeni tukaj, nato pa lahko zanje omogočite uporabo brez povezave. + Ni vsebine predmeta + Vsebina predmeta bo navedena tukaj, nato pa lahko zanjo omogočite uporabo brez povezave. + Želite zavreči spremembe? + Če jih boste zavrgli, spremembe ne bodo shranjene. + Zavrzi + Želite sinhronizirati vsebino brez povezave? + S tem boste sinhronizirali vsebino ~%s Ponudnik podatkov vam lahko zaračuna dodatne stroške, če niste povezani v omrežje Wi-Fi. + S tem boste sinhronizirali vsebino ~%s le, ko ste povezani v omrežje Wi-Fi. + Sinhronizacija + Nalaganje ... + Počakajte, za vas pripravljamo stvari. + Razširi vsebino + Strni vsebino Če omogočite samodejno sinhronizacijo vsebine, boste omogočili prenos izbrane vsebine na podlagi spodnjih nastavitev. Sinhronizacija vsebine bo izvedena tudi, če aplikacija ni zagnana. Če je nastavitev izklopljena, sinhronizacija ne bo izvedena. Že prenesena vsebina ne bo odstranjena. Pogostost sinhronizacije Samodejna sinhronizacija vsebine @@ -1452,7 +1467,6 @@ %d predmet se sinhronizira. %d predmetov se sinhronizira. - Slike vsebine predmeta Ta naloga ni več na voljo. Nimate povezave Trenutno nimate nobenega predmeta, ki bi bil na voljo brez povezave. @@ -1464,4 +1478,5 @@ %d predmet je bil sinhroniziran. %d predmetov je bilo sinhroniziranih. + Dodatna vsebina predmeta diff --git a/libs/pandares/src/main/res/values-sv/strings.xml b/libs/pandares/src/main/res/values-sv/strings.xml index ae0a2f612e..cff370c4bb 100644 --- a/libs/pandares/src/main/res/values-sv/strings.xml +++ b/libs/pandares/src/main/res/values-sv/strings.xml @@ -1410,6 +1410,21 @@ Välj alla Avmarkera alla Ett fel uppstod vid inläsning av innehållet. + Inga kurser + Dina kurser kommer att listas här, och sedan kan du göra dem tillgängliga för offlineanvändning. + Inget kursinnehåll + Kursinnehållet kommer att listas här, och sedan kan du göra dem tillgängliga för offlineanvändning. + Ignorera ändringar? + Om du väljer att avvisa kommer ändringarna inte att sparas. + Avbryt + Synkronisera offlineinnehåll? + Detta synkroniserar ~%s-innehåll. Detta kan medföra ytterligare kostnader från din dataleverantör om du inte är ansluten till ett wifi-nätverk. + Detta synkroniserar ~%s-innehåll endast när du är ansluten till ett wifi-nätverk. + Synkronisera + Läser in ... + Vänta, vi förbereder åt dig. + Visa innehållet + Dölj innehållet Om automatisk innehållssynkronisering aktiveras laddas det valda innehållet ned baserat på nedanstående inställningar. Innehållssynkroniseringen sker även om programmet inte körs. Om inställningen är avstängd sker ingen synkronisering. Innehåll som redan laddats ned raderas inte. Synkroniseringsfrekvens Automatisk innehållssynkronisering @@ -1452,7 +1467,6 @@ %d-kurs synkroniserar. %d-kurser synkroniserar. - Bilder i kursinnehållet Denna uppgift är inte längre tillgänglig. Du är offline Du har för närvarande inte några kurser som är tillgängliga offline. @@ -1464,4 +1478,5 @@ %d-kurs har synkroniserats. %d-kurser har synkroniserats. + Extra kursinnehåll diff --git a/libs/pandares/src/main/res/values-th/strings.xml b/libs/pandares/src/main/res/values-th/strings.xml index 6023ee0943..3696e6d1af 100644 --- a/libs/pandares/src/main/res/values-th/strings.xml +++ b/libs/pandares/src/main/res/values-th/strings.xml @@ -1410,6 +1410,21 @@ เลือกทั้งหมด ยกเลิกการเลือกทั้งหมด เกิดข้อผิดพลาดขณะโหลดเนื้อหา + ไม่มีบทเรียน + บทเรียนของคุณจะปรากฏขึ้นที่นี่ จากนั้นคุณสามารถเผยแพร่สำหรับใช้งานแบบออฟไลน์ + ไม่มีเนื้อหาบทเรียน + เนื้อหาบทเรียนจะถูกแสดงไว้ที่นี่ จากนั้นคุณสามารถเผยแพร่สำหรับการใช้งานออฟไลน์ + ยกเลิกการเปลี่ยนแปลงหรือไม่ + หากคุณเลือกล้มเลิก การเปลี่ยนแปลงจะไม่ถูกบันทึกไว้ + ล้มเลิก + ซิงค์ข้อมูลออฟไลน์หรือไม่ + นี่จะเป็นการซิงค์ข้อมูล ~%s อาจมีค่าบริการเพิ่มเติมจากผู้ให้บริการเครือข่ายหากคุณไม่ได้เชื่อมต่อกับเครือข่าย Wi-Fi + นี่จะเป็นการซิงค์ข้อมูล ~%s เฉพาะในขณะที่คุณเชื่อมต่อกับเครือข่าย Wi-Fi + ซิงค์ + กำลังโหลด... + อดทนไว้ เรากำลังเตรียมการให้กับคุณ + ขยายเนื้อหา + ย่อเนื้อหา การเปิดใช้การซิงค์เนื้อหาอัตโนมัติจะเป็นการจัดการการดาวน์โหลดเนื้อหาที่เลือกตามค่าปรับตั้งต่อไปนี้ การซิงค์เนื้อหาจะเกิดขึ้นแม้ว่าแอพพลิเคชั่นจะไม่เปิดทำงานอยู่ หากมีการปิดค่านี้ จะไม่มีการซิงค์ข้อมูลเกิดขึ้น เนื้อหาที่ดาวน์โหลดแล้วจะไม่ถูกลบทิ้ง ความถี่ในการซิงค์ ซิงค์เนื้อหาอัตโนมัติ @@ -1452,7 +1467,6 @@ %d บทเรียนกำลังซิงค์อยู่ %d บทเรียนกำลังซิงค์อยู่ - ภาพเนื้อหาบทเรียน ภารกิจนี้ไม่มีอยู่อีกต่อไป คุณออฟไลน์อยู่ ปัจจุบันคุณไม่มีบทเรียนแบบออฟไลน์ @@ -1464,4 +1478,5 @@ %d บทเรียนได้รับการซิงค์แล้ว %d บทเรียนได้รับการซิงค์แล้ว + เนื้อหาบทเรียนเพิ่มเติม diff --git a/libs/pandares/src/main/res/values-vi/strings.xml b/libs/pandares/src/main/res/values-vi/strings.xml index 886940b349..444e67677b 100644 --- a/libs/pandares/src/main/res/values-vi/strings.xml +++ b/libs/pandares/src/main/res/values-vi/strings.xml @@ -1411,6 +1411,21 @@ Chọn Tất Cả Bỏ Chọn Tất Cả Đã xảy ra lỗi khi tải nội dung. + Không Có Khóa Học + Khóa học của bạn sẽ được liệt kê ở đây, và sau đó bạn có thể xử lý để sử dụng ngoại tuyến. + Không Có Nội Dung Khóa Học + Nội dung khóa học sẽ được liệt kê ở đây, và sau đó bạn có thể xử lý để sử dụng ngoại tuyến. + Hủy Bỏ Thay Đổi? + Nếu bạn chọn hủy, các thay đổi sẽ không được lưu. + Hủy Bỏ + Đồng Bộ Hóa Nội Dung Ngoại Tuyến? + Thao tác này sẽ đồng bộ ~%s nội dung. Điều này có thể khiến bạn bị tính thêm phí từ nhà cung cấp dữ liệu của bạn, nếu bạn không kết nối với mạng Wi-Fi. + Thao tác này sẽ đồng bộ ~%s nội dung chỉ khi bạn kết nối với mạng Wi-Fi. + Đồng bộ + Đang tải... + Hãy chờ nhé, chúng tôi đang chuẩn bị mọi thứ cho bạn. + Mở rộng nội dung + Thu gọn nội dung Bật chức năng Tự Động Đồng Bộ Nội Dung sẽ xử lý việc tải xuống các nội dung được chọn dựa theo cài đặt dưới đây. Thao tác đồng bộ nội dung sẽ diễn ra ngay cả khi ứng dụng không chạy. Nếu tắt cài đặt thì quá trình đồng bộ sẽ không diễn ra. Nội dung đã được tải xuống sẽ không bị xóa. Tần Suất Đồng Bộ Tự Động Đồng Bộ Nội Dung @@ -1453,7 +1468,6 @@ %d khóa học đang đồng bộ. %d khóa học đang đồng bộ. - Hình ảnh nội dung khóa học Bài tập này không còn khả dụng. Bạn đang ngoại tuyến Hiện tại bạn không có bất kỳ khóa học nào khả dụng ngoại tuyến. @@ -1465,4 +1479,5 @@ %d khóa học đã được đồng bộ. %d khóa học đã được đồng bộ. + Nội dung khóa học bổ sung diff --git a/libs/pandares/src/main/res/values-zh/strings.xml b/libs/pandares/src/main/res/values-zh/strings.xml index f9e4a98069..d2e4fdcb21 100644 --- a/libs/pandares/src/main/res/values-zh/strings.xml +++ b/libs/pandares/src/main/res/values-zh/strings.xml @@ -1392,6 +1392,21 @@ 全选 取消全选 加载内容时出错。 + 没有课程 + 此处将列出您的课程,然后您可以使内容离线可用。 + 无课程内容 + 此处将列出课程内容,然后您可以使内容离线可用。 + 放弃更改? + 如果您选择放弃,更改将不会保存。 + 放弃 + 同步离线内容? + 大约会有 %s 项内容同步。如果未连接到无线网络,您的数据流量提供商可能会额外收费。 + 仅当连接到无线网络时,大约会有 %s 项内容同步。 + 同步 + 加载中... + 正在准备中,请稍等。 + 扩展内容 + 折叠内容 如果启用“自动同步内容”,将根据以下设置下载所选内容。即使应用程序没有运行,内容也会同步。如果关闭该设置,将不会同步。已下载的内容不会被删除。 同步周期 自动同步内容 @@ -1433,7 +1448,6 @@ %d 门课程正在同步。 - 课程内容图像 此作业不再可用。 您已离线 您目前没有任何可离线使用的课程。 @@ -1444,4 +1458,5 @@ %d 门课程已同步。 + 更多课程内容 diff --git a/libs/recyclerview/src/main/java/com/instructure/pandarecycler/BaseRecyclerAdapter.kt b/libs/recyclerview/src/main/java/com/instructure/pandarecycler/BaseRecyclerAdapter.kt index 6365e3f807..71cf71916f 100644 --- a/libs/recyclerview/src/main/java/com/instructure/pandarecycler/BaseRecyclerAdapter.kt +++ b/libs/recyclerview/src/main/java/com/instructure/pandarecycler/BaseRecyclerAdapter.kt @@ -22,7 +22,7 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -abstract class BaseRecyclerAdapter(var context: Context) : RecyclerView.Adapter() { +abstract class BaseRecyclerAdapter(var context: Context) : RecyclerView.Adapter() { abstract fun createViewHolder(v: View, viewType: Int): T abstract fun itemLayoutResId(viewType: Int): Int abstract fun loadData()