Commit 4d752ee9 authored by Lorenzo Pichilli's avatar Lorenzo Pichilli

updated ChromeSafariBrowser class, Renamed Chrome Custom Tab addShareButton...

updated ChromeSafariBrowser class, Renamed Chrome Custom Tab addShareButton option to addDefaultShareMenuItem, Renamed ChromeSafariBrowser onLoaded to onCompletedInitialLoad, Renamed all iOS and Android webview options class, fix #229, Added packageName and keepAliveEnabled ChromeCustomTab options for Android
parent 9c7ac0da
......@@ -11,6 +11,7 @@
- Added `reloadFromOrigin` webview method for iOS
- Added `automaticallyAdjustsScrollIndicatorInsets` webview options for iOS
- Added `WebStorageManager` class which manages the web storage used by WebView instances
- Added `packageName` [#229](https://github.com/pichillilorenzo/flutter_inappwebview/issues/229) and `keepAliveEnabled` ChromeCustomTab options for Android
- Updated for Flutter 1.12 new Java Embedding API (Android)
- Updated `clearCache` for Android
- Updated default value for `domStorageEnabled` and `databaseEnabled` options to `true` for Android
......@@ -34,7 +35,9 @@
- Renamed `onPermissionRequest` to `androidOnPermissionRequest`
- Updated attribute names for `InAppWebViewWidgetOptions`, `InAppBrowserClassOptions` and `ChromeSafariBrowserClassOptions` classes
- Renamed and updated `onNavigationStateChange` to `onUpdateVisitedHistory`
- Renamed all iOS options prefix from `Ios` to `IOS`
- Renamed all iOS and Android webview options class
- Renamed Chrome Custom Tab `addShareButton` option to `addDefaultShareMenuItem`
- Renamed ChromeSafariBrowser `onLoaded` to `onCompletedInitialLoad`
## 2.1.0+1
......
......@@ -641,8 +641,8 @@ Specific options of the `InAppBrowser` class are:
* `toolbarBottomTranslucent`: Set to `true` to set the toolbar at the bottom translucent. The default value is `true`.
* `closeButtonCaption`: Set the custom text for the close button.
* `closeButtonColor`: Set the custom color for the close button.
* `presentationStyle`: Set the custom modal presentation style when presenting the WebView. The default value is `IosWebViewOptionsPresentationStyle.FULL_SCREEN`.
* `transitionStyle`: Set to the custom transition style when presenting the WebView. The default value is `IosWebViewOptionsTransitionStyle.COVER_VERTICAL`.
* `presentationStyle`: Set the custom modal presentation style when presenting the WebView. The default value is `IOSUIModalPresentationStyle.FULL_SCREEN`.
* `transitionStyle`: Set to the custom transition style when presenting the WebView. The default value is `IOSUIModalTransitionStyle.COVER_VERTICAL`.
* `spinner`: Set to `false` to hide the spinner when the WebView is loading a page. The default value is `true`.
#### `InAppBrowser` Events
......@@ -698,8 +698,8 @@ class MyChromeSafariBrowser extends ChromeSafariBrowser {
}
@override
void onLoaded() {
print("ChromeSafari browser loaded");
void onCompletedInitialLoad() {
print("ChromeSafari browser initial load completed");
}
@override
......@@ -737,7 +737,7 @@ class _MyAppState extends State<MyApp> {
await widget.browser.open(
url: "https://flutter.dev/",
options: ChromeSafariBrowserClassOptions(
android: AndroidChromeCustomTabsOptions(addShareButton: false),
android: AndroidChromeCustomTabsOptions(addDefaultShareMenuItem: false),
ios: IosSafariOptions(barCollapsingEnabled: true)));
},
child: Text("Open Chrome Safari Browser")),
......@@ -767,26 +767,28 @@ Screenshots:
##### `ChromeSafariBrowser` Android-specific options
* `addShareButton`: Set to `false` if you don't want the default share button. The default value is `true`.
* `addDefaultShareMenuItem`: Set to `false` if you don't want the default share item to the menu. The default value is `true`.
* `showTitle`: Set to `false` if the title shouldn't be shown in the custom tab. The default value is `true`.
* `toolbarBackgroundColor`: Set the custom background color of the toolbar.
* `enableUrlBarHiding`: Set to `true` to enable the url bar to hide as the user scrolls down on the page. The default value is `false`.
* `instantAppsEnabled`: Set to `true` to enable Instant Apps. The default value is `false`.
* `packageName`: Set the name of the application package to handle the intent (for example `com.android.chrome`), or null to allow any application package.
* `keepAliveEnabled`: Set to `true` to enable Keep Alive. The default value is `false`.
##### `ChromeSafariBrowser` iOS-specific options
* `entersReaderIfAvailable`: Set to `true` if Reader mode should be entered automatically when it is available for the webpage. The default value is `false`.
* `barCollapsingEnabled`: Set to `true` to enable bar collapsing. The default value is `false`.
* `dismissButtonStyle`: Set the custom style for the dismiss button. The default value is `IosSafariOptionsDismissButtonStyle.DONE`.
* `dismissButtonStyle`: Set the custom style for the dismiss button. The default value is `IOSSafariDismissButtonStyle.DONE`.
* `preferredBarTintColor`: Set the custom background color of the navigation bar and the toolbar.
* `preferredControlTintColor`: Set the custom color of the control buttons on the navigation bar and the toolbar.
* `presentationStyle`: Set the custom modal presentation style when presenting the WebView. The default value is `IosWebViewOptionsPresentationStyle.FULL_SCREEN`.
* `transitionStyle`: Set to the custom transition style when presenting the WebView. The default value is `IosWebViewOptionsTransitionStyle.COVER_VERTICAL`.
* `presentationStyle`: Set the custom modal presentation style when presenting the WebView. The default value is `IOSUIModalPresentationStyle.FULL_SCREEN`.
* `transitionStyle`: Set to the custom transition style when presenting the WebView. The default value is `IOSUIModalTransitionStyle.COVER_VERTICAL`.
#### `ChromeSafariBrowser` Events
* `onOpened`: Event fires when the `ChromeSafariBrowser` is opened.
* `onLoaded`: Event fires when the `ChromeSafariBrowser` is loaded.
* `onCompletedInitialLoad`: Event fires when the initial URL load is complete.
* `onClosed`: Event fires when the `ChromeSafariBrowser` is closed.
### `InAppLocalhostServer` class
......
......@@ -5,7 +5,12 @@ import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsService;
import androidx.browser.customtabs.CustomTabsSession;
import com.pichillilorenzo.flutter_inappwebview.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview.R;
......@@ -20,7 +25,10 @@ public class ChromeCustomTabsActivity extends Activity {
CustomTabsIntent.Builder builder;
ChromeCustomTabsOptions options;
private CustomTabActivityHelper customTabActivityHelper;
private CustomTabsSession customTabsSession;
private final int CHROME_CUSTOM_TAB_REQUEST_CODE = 100;
private boolean onChromeSafariBrowserOpened = false;
private boolean onChromeSafariBrowserCompletedInitialLoad = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
......@@ -31,30 +39,82 @@ public class ChromeCustomTabsActivity extends Activity {
Bundle b = getIntent().getExtras();
assert b != null;
uuid = b.getString("uuid");
String url = b.getString("url");
final String url = b.getString("url");
options = new ChromeCustomTabsOptions();
options.parse((HashMap<String, Object>) b.getSerializable("options"));
InAppWebViewFlutterPlugin.inAppBrowser.chromeCustomTabsActivities.put(uuid, this);
customTabActivityHelper = new CustomTabActivityHelper();
builder = new CustomTabsIntent.Builder();
final ChromeCustomTabsActivity chromeCustomTabsActivity = this;
prepareCustomTabs();
customTabActivityHelper = new CustomTabActivityHelper();
customTabActivityHelper.setConnectionCallback(new CustomTabActivityHelper.ConnectionCallback() {
@Override
public void onCustomTabsConnected() {
customTabsSession = customTabActivityHelper.getSession();
Uri uri = Uri.parse(url);
customTabActivityHelper.mayLaunchUrl(uri, null, null);
builder = new CustomTabsIntent.Builder(customTabsSession);
CustomTabsIntent customTabsIntent = builder.build();
prepareCustomTabs(customTabsIntent);
CustomTabActivityHelper.openCustomTab(chromeCustomTabsActivity, customTabsIntent, uri, CHROME_CUSTOM_TAB_REQUEST_CODE);
}
CustomTabActivityHelper.openCustomTab(this, customTabsIntent, Uri.parse(url), CHROME_CUSTOM_TAB_REQUEST_CODE);
@Override
public void onCustomTabsDisconnected() {
customTabsSession = null;
finish();
Map<String, Object> obj = new HashMap<>();
obj.put("uuid", uuid);
InAppWebViewFlutterPlugin.inAppBrowser.channel.invokeMethod("onChromeSafariBrowserClosed", obj);
}
});
customTabActivityHelper.setCustomTabsCallback(new CustomTabsCallback() {
@Override
public void onNavigationEvent(int navigationEvent, Bundle extras) {
if (navigationEvent == TAB_SHOWN && !onChromeSafariBrowserOpened) {
onChromeSafariBrowserOpened = true;
Map<String, Object> obj = new HashMap<>();
obj.put("uuid", uuid);
InAppWebViewFlutterPlugin.inAppBrowser.channel.invokeMethod("onChromeSafariBrowserOpened", obj);
InAppWebViewFlutterPlugin.inAppBrowser.channel.invokeMethod("onChromeSafariBrowserLoaded", obj);
}
private void prepareCustomTabs() {
if (options.addShareButton)
if (navigationEvent == NAVIGATION_FINISHED && !onChromeSafariBrowserCompletedInitialLoad) {
onChromeSafariBrowserCompletedInitialLoad = true;
Map<String, Object> obj = new HashMap<>();
obj.put("uuid", uuid);
InAppWebViewFlutterPlugin.inAppBrowser.channel.invokeMethod("onChromeSafariBrowserCompletedInitialLoad", obj);
}
}
@Override
public void extraCallback(String callbackName, Bundle args) {
}
@Override
public void onMessageChannelReady(Bundle extras) {
}
@Override
public void onPostMessage(String message, Bundle extras) {
}
@Override
public void onRelationshipValidationResult(@CustomTabsService.Relation int relation, Uri requestedOrigin,
boolean result, Bundle extras) {
}
});
}
private void prepareCustomTabs(CustomTabsIntent customTabsIntent) {
if (options.addDefaultShareMenuItem)
builder.addDefaultShareMenuItem();
if (!options.toolbarBackgroundColor.isEmpty())
......@@ -66,6 +126,14 @@ public class ChromeCustomTabsActivity extends Activity {
builder.enableUrlBarHiding();
builder.setInstantAppsEnabled(options.instantAppsEnabled);
if (options.packageName != null)
customTabsIntent.intent.setPackage(options.packageName);
else
customTabsIntent.intent.setPackage(CustomTabsHelper.getPackageNameToUse(this));
if (options.keepAliveEnabled)
CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent);
}
@Override
......@@ -83,6 +151,7 @@ public class ChromeCustomTabsActivity extends Activity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CHROME_CUSTOM_TAB_REQUEST_CODE) {
customTabsSession = null;
finish();
Map<String, Object> obj = new HashMap<>();
obj.put("uuid", uuid);
......
......@@ -6,10 +6,12 @@ public class ChromeCustomTabsOptions extends Options {
final static String LOG_TAG = "ChromeCustomTabsOptions";
public boolean addShareButton = true;
public boolean showTitle = true;
public Boolean addDefaultShareMenuItem = true;
public Boolean showTitle = true;
public String toolbarBackgroundColor = "";
public boolean enableUrlBarHiding = false;
public boolean instantAppsEnabled = false;
public Boolean enableUrlBarHiding = false;
public Boolean instantAppsEnabled = false;
public String packageName;
public Boolean keepAliveEnabled = false;
}
......@@ -3,6 +3,8 @@ package com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsServiceConnection;
......@@ -18,6 +20,7 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
private CustomTabsClient mClient;
private CustomTabsServiceConnection mConnection;
private ConnectionCallback mConnectionCallback;
private CustomTabsCallback mCustomTabsCallback;
/**
* Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView.
......@@ -59,7 +62,7 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
if (mClient == null) {
mCustomTabsSession = null;
} else if (mCustomTabsSession == null) {
mCustomTabsSession = mClient.newSession(null);
mCustomTabsSession = mClient.newSession(mCustomTabsCallback);
}
return mCustomTabsSession;
}
......@@ -72,6 +75,10 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
this.mConnectionCallback = connectionCallback;
}
public void setCustomTabsCallback(CustomTabsCallback customTabsCallback) {
this.mCustomTabsCallback = customTabsCallback;
}
/**
* Binds the Activity to the Custom Tabs Service.
* @param activity the activity to be binded to the service.
......
......@@ -9,6 +9,8 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import androidx.browser.customtabs.CustomTabsService;
import java.util.ArrayList;
import java.util.List;
......@@ -23,8 +25,6 @@ public class CustomTabsHelper {
static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
"android.support.customtabs.extra.KEEP_ALIVE";
private static final String ACTION_CUSTOM_TABS_CONNECTION =
"android.support.customtabs.action.CustomTabsService";
private static String sPackageNameToUse;
......@@ -63,7 +63,7 @@ public class CustomTabsHelper {
List<String> packagesSupportingCustomTabs = new ArrayList<>();
for (ResolveInfo info : resolvedActivityList) {
Intent serviceIntent = new Intent();
serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION);
serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
serviceIntent.setPackage(info.activityInfo.packageName);
if (pm.resolveService(serviceIntent, 0) != null) {
packagesSupportingCustomTabs.add(info.activityInfo.packageName);
......
......@@ -4,13 +4,13 @@ public class InAppBrowserOptions extends Options {
public static final String LOG_TAG = "InAppBrowserOptions";
public boolean hidden = false;
public boolean toolbarTop = true;
public Boolean hidden = false;
public Boolean toolbarTop = true;
public String toolbarTopBackgroundColor = "";
public String toolbarTopFixedTitle = "";
public boolean hideUrlBar = false;
public Boolean hideUrlBar = false;
public boolean hideTitleBar = false;
public boolean closeOnCannotGoBack = true;
public boolean progressBar = true;
public Boolean hideTitleBar = false;
public Boolean closeOnCannotGoBack = true;
public Boolean progressBar = true;
}
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
......
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
......
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
......
......@@ -12,8 +12,8 @@ class MyChromeSafariBrowser extends ChromeSafariBrowser {
}
@override
void onLoaded() {
print("ChromeSafari browser loaded");
void onCompletedInitialLoad() {
print("ChromeSafari browser initial load completed");
}
@override
......@@ -52,7 +52,7 @@ class _ChromeSafariBrowserExampleScreenState
await widget.browser.open(
url: "https://flutter.dev/",
options: ChromeSafariBrowserClassOptions(
android: AndroidChromeCustomTabsOptions(addShareButton: false),
android: AndroidChromeCustomTabsOptions(addDefaultShareMenuItem: false, keepAliveEnabled: true),
ios: IOSSafariOptions(barCollapsingEnabled: true)));
},
child: Text("Open Chrome Safari Browser")),
......
......@@ -71,29 +71,6 @@ class _InAppWebViewExampleScreenState extends State<InAppWebViewExampleScreen> {
setState(() {
this.url = url;
});
/*var origins = await WebStorageManager.instance().android.getOrigins();
for (var origin in origins) {
print(origin);
print(await WebStorageManager.instance().android.getQuotaForOrigin(origin: origin.origin));
print(await WebStorageManager.instance().android.getUsageForOrigin(origin: origin.origin));
}
await WebStorageManager.instance().android.deleteAllData();
print("\n\nDELETED\n\n");
origins = await WebStorageManager.instance().android.getOrigins();
for (var origin in origins) {
print(origin);
await WebStorageManager.instance().android.deleteOrigin(origin: origin.origin);
}*/
/*var records = await WebStorageManager.instance().ios.fetchDataRecords(dataTypes: IOSWKWebsiteDataType.ALL);
for(var record in records) {
print(record);
}
await WebStorageManager.instance().ios.removeDataModifiedSince(dataTypes: IOSWKWebsiteDataType.ALL, date: DateTime(0));
print("\n\nDELETED\n\n");
records = await WebStorageManager.instance().ios.fetchDataRecords(dataTypes: IOSWKWebsiteDataType.ALL);
for(var record in records) {
print(record);
}*/
},
onProgressChanged: (InAppWebViewController controller, int progress) {
setState(() {
......
......@@ -1036,8 +1036,6 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
dataDetectorTypes = WKDataDetectorTypes(rawValue: dataDetectorTypes.rawValue | dataDetectorType.rawValue)
}
configuration.dataDetectorTypes = dataDetectorTypes
} else {
// Fallback on earlier versions
}
if #available(iOS 13.0, *) {
......@@ -1046,8 +1044,6 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
configuration.defaultWebpagePreferences.preferredContentMode = WKWebpagePreferences.ContentMode(rawValue: (options?.preferredContentMode)!)!
}
scrollView.automaticallyAdjustsScrollIndicatorInsets = (options?.automaticallyAdjustsScrollIndicatorInsets)!
} else {
// Fallback on earlier versions
}
scrollView.showsVerticalScrollIndicator = (options?.verticalScrollBarEnabled)!
......@@ -1110,8 +1106,6 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
configuration.setURLSchemeHandler(CustomeSchemeHandler(), forURLScheme: scheme)
}
}
} else {
// Fallback on earlier versions
}
return configuration
......@@ -1335,12 +1329,8 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
}
configuration.dataDetectorTypes = dataDetectorTypes
}
} else {
// Fallback on earlier versions
}
scrollView
if #available(iOS 13.0, *) {
if newOptionsMap["isFraudulentWebsiteWarningEnabled"] != nil && options?.isFraudulentWebsiteWarningEnabled != newOptions.isFraudulentWebsiteWarningEnabled {
configuration.preferences.isFraudulentWebsiteWarningEnabled = newOptions.isFraudulentWebsiteWarningEnabled
......@@ -1351,8 +1341,6 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
if newOptionsMap["automaticallyAdjustsScrollIndicatorInsets"] != nil && options?.automaticallyAdjustsScrollIndicatorInsets != newOptions.automaticallyAdjustsScrollIndicatorInsets {
scrollView.automaticallyAdjustsScrollIndicatorInsets = newOptions.automaticallyAdjustsScrollIndicatorInsets
}
} else {
// Fallback on earlier versions
}
if newOptionsMap["verticalScrollBarEnabled"] != nil && options?.verticalScrollBarEnabled != newOptions.verticalScrollBarEnabled {
......
......@@ -59,13 +59,32 @@ class SafariViewController: SFSafariViewController, SFSafariViewControllerDelega
func safariViewController(_ controller: SFSafariViewController,
didCompleteInitialLoad didLoadSuccessfully: Bool) {
if didLoadSuccessfully {
statusDelegate?.onChromeSafariBrowserLoaded(uuid: self.uuid)
statusDelegate?.onChromeSafariBrowserCompletedInitialLoad(uuid: self.uuid)
}
else {
print("Cant load successfully the 'SafariViewController'.")
}
}
func safariViewController(_ controller: SFSafariViewController, activityItemsFor URL: URL, title: String?) -> [UIActivity] {
// print("activityItemsFor")
// print(URL)
// print(title)
return []
}
func safariViewController(_ controller: SFSafariViewController, excludedActivityTypesFor URL: URL, title: String?) -> [UIActivity.ActivityType] {
// print("excludedActivityTypesFor")
// print(URL)
// print(title)
return []
}
func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo URL: URL) {
// print("initialLoadDidRedirectTo")
// print(URL)
}
// Helper function to convert hex color string to UIColor
// Assumes input like "#00FF00" (#RRGGBB).
// Taken from https://stackoverflow.com/questions/1560081/how-can-i-create-a-uicolor-from-a-hex-string
......
......@@ -432,11 +432,10 @@ public class SwiftFlutterPlugin: NSObject, FlutterPlugin {
safari.safariOptions = safariOptions
self.safariViewControllers[uuid] = safari
tmpController.present(self.safariViewControllers[uuid]! as! SFSafariViewController, animated: true)
onChromeSafariBrowserOpened(uuid: uuid)
tmpController.present(self.safariViewControllers[uuid]! as! SFSafariViewController, animated: true) {
self.onChromeSafariBrowserOpened(uuid: uuid)
result(true)
}
return
}
else {
......@@ -742,9 +741,9 @@ public class SwiftFlutterPlugin: NSObject, FlutterPlugin {
}
}
public func onChromeSafariBrowserLoaded(uuid: String) {
public func onChromeSafariBrowserCompletedInitialLoad(uuid: String) {
if self.safariViewControllers[uuid] != nil {
self.channel!.invokeMethod("onChromeSafariBrowserLoaded", arguments: ["uuid": uuid])
self.channel!.invokeMethod("onChromeSafariBrowserCompletedInitialLoad", arguments: ["uuid": uuid])
}
}
......
//
// TestWebView.swift
// flutter_inappwebview
//
// Created by Lorenzo Pichilli on 14/12/2019.
//
import Flutter
import Foundation
import WebKit
public class TestWebView: WKWebView {
override init(frame: CGRect, configuration: WKWebViewConfiguration) {
super.init(frame: frame, configuration: configuration)
}
required public init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
public override func removeFromSuperview() {
configuration.userContentController.removeAllUserScripts()
super.removeFromSuperview()
print("\n\n DISPOSE \n\n")
}
deinit {
print("dealloc") // never called
}
}
......@@ -32,8 +32,8 @@ class ChromeSafariBrowser {
case "onChromeSafariBrowserOpened":
onOpened();
break;
case "onChromeSafariBrowserLoaded":
onLoaded();
case "onChromeSafariBrowserCompletedInitialLoad":
onCompletedInitialLoad();
break;
case "onChromeSafariBrowserClosed":
onClosed();
......@@ -109,8 +109,8 @@ class ChromeSafariBrowser {
///Event fires when the [ChromeSafariBrowser] is opened.
void onOpened() {}
///Event fires when the [ChromeSafariBrowser] is loaded.
void onLoaded() {}
///Event fires when the initial URL load is complete.
void onCompletedInitialLoad() {}
///Event fires when the [ChromeSafariBrowser] is closed.
void onClosed() {}
......
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment