未验证 提交 9671f634 编写于 作者: M Mikkel Nygaard Ravn 提交者: GitHub

Make standard codecs extensible (#4770)

上级 7d9e42ac
......@@ -58,13 +58,12 @@ import android.util.Log;
* branch. BigIntegers were needed because the Dart 1.0 int type had no size
* limit. With Dart 2.0, the int type is a fixed-size, 64-bit signed integer.
* If you need to communicate larger integers, use String encoding instead.</p>
*
* <p>To extend the codec, overwrite the writeValue and readValueOfType methods.</p>
*/
public final class StandardMessageCodec implements MessageCodec<Object> {
public class StandardMessageCodec implements MessageCodec<Object> {
public static final StandardMessageCodec INSTANCE = new StandardMessageCodec();
private StandardMessageCodec() {
}
@Override
public ByteBuffer encodeMessage(Object message) {
if (message == null) {
......@@ -108,7 +107,11 @@ public final class StandardMessageCodec implements MessageCodec<Object> {
private static final byte LIST = 12;
private static final byte MAP = 13;
private static void writeSize(ByteArrayOutputStream stream, int value) {
/**
* Writes an int representing a size to the specified stream.
* Uses an expanding code of 1 to 5 bytes to optimize for small values.
*/
protected static final void writeSize(ByteArrayOutputStream stream, int value) {
assert 0 <= value;
if (value < 254) {
stream.write(value);
......@@ -121,7 +124,11 @@ public final class StandardMessageCodec implements MessageCodec<Object> {
}
}
private static void writeChar(ByteArrayOutputStream stream, int value) {
/**
* Writes the least significant two bytes of the specified int to the
* specified stream.
*/
protected static final void writeChar(ByteArrayOutputStream stream, int value) {
if (LITTLE_ENDIAN) {
stream.write(value);
stream.write(value >>> 8);
......@@ -131,7 +138,10 @@ public final class StandardMessageCodec implements MessageCodec<Object> {
}
}
private static void writeInt(ByteArrayOutputStream stream, int value) {
/**
* Writes the specified int as 4 bytes to the specified stream.
*/
protected static final void writeInt(ByteArrayOutputStream stream, int value) {
if (LITTLE_ENDIAN) {
stream.write(value);
stream.write(value >>> 8);
......@@ -145,7 +155,10 @@ public final class StandardMessageCodec implements MessageCodec<Object> {
}
}
private static void writeLong(ByteArrayOutputStream stream, long value) {
/**
* Writes the specified long as 8 bytes to the specified stream.
*/
protected static final void writeLong(ByteArrayOutputStream stream, long value) {
if (LITTLE_ENDIAN) {
stream.write((byte) value);
stream.write((byte) (value >>> 8));
......@@ -167,16 +180,29 @@ public final class StandardMessageCodec implements MessageCodec<Object> {
}
}
private static void writeDouble(ByteArrayOutputStream stream, double value) {
/**
* Writes the specified double as 8 bytes to the specified stream.
*/
protected static final void writeDouble(ByteArrayOutputStream stream, double value) {
writeLong(stream, Double.doubleToLongBits(value));
}
private static void writeBytes(ByteArrayOutputStream stream, byte[] bytes) {
/**
* Writes the length and then the actual bytes of the specified array to
* the specified stream.
*/
protected static final void writeBytes(ByteArrayOutputStream stream, byte[] bytes) {
writeSize(stream, bytes.length);
stream.write(bytes, 0, bytes.length);
}
private static void writeAlignment(ByteArrayOutputStream stream, int alignment) {
/**
* Writes a number of padding bytes to the specified stream to ensure that
* the next value is aligned to a whole multiple of the specified alignment.
* An example usage with alignment = 8 is to ensure doubles are word-aligned
* in the stream.
*/
protected static final void writeAlignment(ByteArrayOutputStream stream, int alignment) {
final int mod = stream.size() % alignment;
if (mod != 0) {
for (int i = 0; i < alignment - mod; i++) {
......@@ -185,7 +211,14 @@ public final class StandardMessageCodec implements MessageCodec<Object> {
}
}
static void writeValue(ByteArrayOutputStream stream, Object value) {
/**
* Writes a type discriminator byte and then a byte serialization of the
* specified value to the specified stream.
*
* <p>Subclasses can extend the codec by overriding this method, calling
* super for values that the extension does not handle.</p>
*/
protected void writeValue(ByteArrayOutputStream stream, Object value) {
if (value == null) {
stream.write(NULL);
} else if (value == Boolean.TRUE) {
......@@ -261,7 +294,10 @@ public final class StandardMessageCodec implements MessageCodec<Object> {
}
}
private static int readSize(ByteBuffer buffer) {
/**
* Reads an int representing a size as written by writeSize.
*/
protected static final int readSize(ByteBuffer buffer) {
if (!buffer.hasRemaining()) {
throw new IllegalArgumentException("Message corrupted");
}
......@@ -275,26 +311,46 @@ public final class StandardMessageCodec implements MessageCodec<Object> {
}
}
private static byte[] readBytes(ByteBuffer buffer) {
/**
* Reads a byte array as written by writeBytes.
*/
protected static final byte[] readBytes(ByteBuffer buffer) {
final int length = readSize(buffer);
final byte[] bytes = new byte[length];
buffer.get(bytes);
return bytes;
}
private static void readAlignment(ByteBuffer buffer, int alignment) {
/**
* Reads alignment padding bytes as written by writeAlignment.
*/
protected static final void readAlignment(ByteBuffer buffer, int alignment) {
final int mod = buffer.position() % alignment;
if (mod != 0) {
buffer.position(buffer.position() + alignment - mod);
}
}
static Object readValue(ByteBuffer buffer) {
/**
* Reads a value as written by writeValue.
*/
protected final Object readValue(ByteBuffer buffer) {
if (!buffer.hasRemaining()) {
throw new IllegalArgumentException("Message corrupted");
}
final byte type = buffer.get();
return readValueOfType(type, buffer);
}
/**
* Reads a value of the specified type.
*
* <p>Subclasses may extend the codec by overriding this method, calling
* super for types that the extension does not handle.</p>
*/
protected Object readValueOfType(byte type, ByteBuffer buffer) {
final Object result;
switch (buffer.get()) {
switch (type) {
case NULL:
result = null;
break;
......@@ -374,8 +430,7 @@ public final class StandardMessageCodec implements MessageCodec<Object> {
result = map;
break;
}
default:
throw new IllegalArgumentException("Message corrupted");
default: throw new IllegalArgumentException("Message corrupted");
}
return result;
}
......
......@@ -19,16 +19,21 @@ import java.nio.ByteOrder;
* {@link StandardMessageCodec}.</p>
*/
public final class StandardMethodCodec implements MethodCodec {
public static final StandardMethodCodec INSTANCE = new StandardMethodCodec();
public static final StandardMethodCodec INSTANCE = new StandardMethodCodec(StandardMessageCodec.INSTANCE);
private final StandardMessageCodec messageCodec;
private StandardMethodCodec() {
/**
* Creates a new method codec based on the specified message codec.
*/
public StandardMethodCodec(StandardMessageCodec messageCodec) {
this.messageCodec = messageCodec;
}
@Override
public ByteBuffer encodeMethodCall(MethodCall methodCall) {
final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream();
StandardMessageCodec.writeValue(stream, methodCall.method);
StandardMessageCodec.writeValue(stream, methodCall.arguments);
messageCodec.writeValue(stream, methodCall.method);
messageCodec.writeValue(stream, methodCall.arguments);
final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size());
buffer.put(stream.buffer(), 0, stream.size());
return buffer;
......@@ -37,8 +42,8 @@ public final class StandardMethodCodec implements MethodCodec {
@Override
public MethodCall decodeMethodCall(ByteBuffer methodCall) {
methodCall.order(ByteOrder.nativeOrder());
final Object method = StandardMessageCodec.readValue(methodCall);
final Object arguments = StandardMessageCodec.readValue(methodCall);
final Object method = messageCodec.readValue(methodCall);
final Object arguments = messageCodec.readValue(methodCall);
if (method instanceof String && !methodCall.hasRemaining()) {
return new MethodCall((String) method, arguments);
}
......@@ -49,7 +54,7 @@ public final class StandardMethodCodec implements MethodCodec {
public ByteBuffer encodeSuccessEnvelope(Object result) {
final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream();
stream.write(0);
StandardMessageCodec.writeValue(stream, result);
messageCodec.writeValue(stream, result);
final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size());
buffer.put(stream.buffer(), 0, stream.size());
return buffer;
......@@ -60,9 +65,9 @@ public final class StandardMethodCodec implements MethodCodec {
Object errorDetails) {
final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream();
stream.write(1);
StandardMessageCodec.writeValue(stream, errorCode);
StandardMessageCodec.writeValue(stream, errorMessage);
StandardMessageCodec.writeValue(stream, errorDetails);
messageCodec.writeValue(stream, errorCode);
messageCodec.writeValue(stream, errorMessage);
messageCodec.writeValue(stream, errorDetails);
final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size());
buffer.put(stream.buffer(), 0, stream.size());
return buffer;
......@@ -74,15 +79,15 @@ public final class StandardMethodCodec implements MethodCodec {
final byte flag = envelope.get();
switch (flag) {
case 0: {
final Object result = StandardMessageCodec.readValue(envelope);
final Object result = messageCodec.readValue(envelope);
if (!envelope.hasRemaining()) {
return result;
}
}
case 1: {
final Object code = StandardMessageCodec.readValue(envelope);
final Object message = StandardMessageCodec.readValue(envelope);
final Object details = StandardMessageCodec.readValue(envelope);
final Object code = messageCodec.readValue(envelope);
final Object message = messageCodec.readValue(envelope);
final Object details = messageCodec.readValue(envelope);
if (code instanceof String
&& (message == null || message instanceof String)
&& !envelope.hasRemaining()) {
......
......@@ -48,7 +48,7 @@ FLUTTER_EXPORT
On the Dart side, messages are represented using `ByteData`.
*/
FLUTTER_EXPORT
@interface FlutterBinaryCodec : NSObject<FlutterMessageCodec>
@interface FlutterBinaryCodec : NSObject <FlutterMessageCodec>
@end
/**
......@@ -59,7 +59,7 @@ FLUTTER_EXPORT
on the Dart side. These parts of the Flutter SDK are evolved synchronously.
*/
FLUTTER_EXPORT
@interface FlutterStringCodec : NSObject<FlutterMessageCodec>
@interface FlutterStringCodec : NSObject <FlutterMessageCodec>
@end
/**
......@@ -77,7 +77,57 @@ FLUTTER_EXPORT
package.
*/
FLUTTER_EXPORT
@interface FlutterJSONMessageCodec : NSObject<FlutterMessageCodec>
@interface FlutterJSONMessageCodec : NSObject <FlutterMessageCodec>
@end
/**
A writer of the Flutter standard binary encoding.
See `FlutterStandardMessageCodec` for details on the encoding.
The encoding is extensible via subclasses overriding `writeValue`.
*/
FLUTTER_EXPORT
@interface FlutterStandardWriter : NSObject
- (instancetype)initWithData:(NSMutableData*)data;
- (void)writeByte:(UInt8)value;
- (void)writeBytes:(const void*)bytes length:(NSUInteger)length;
- (void)writeData:(NSData*)data;
- (void)writeSize:(UInt32)size;
- (void)writeAlignment:(UInt8)alignment;
- (void)writeUTF8:(NSString*)value;
- (void)writeValue:(id)value;
@end
/**
A reader of the Flutter standard binary encoding.
See `FlutterStandardMessageCodec` for details on the encoding.
The encoding is extensible via subclasses overriding `readValueOfType`.
*/
FLUTTER_EXPORT
@interface FlutterStandardReader : NSObject
- (instancetype)initWithData:(NSData*)data;
- (BOOL)hasMore;
- (UInt8)readByte;
- (void)readBytes:(void*)destination length:(NSUInteger)length;
- (NSData*)readData:(NSUInteger)length;
- (UInt32)readSize;
- (void)readAlignment:(UInt8)alignment;
- (NSString*)readUTF8;
- (id)readValue;
- (id)readValueOfType:(UInt8)type;
@end
/**
A factory of compatible reader/writer instances using the Flutter standard
binary encoding or extensions thereof.
*/
FLUTTER_EXPORT
@interface FlutterStandardReaderWriter : NSObject
- (FlutterStandardWriter*)writerWithData:(NSMutableData*)data;
- (FlutterStandardReader*)readerWithData:(NSData*)data;
@end
/**
......@@ -113,7 +163,8 @@ FLUTTER_EXPORT
instead.
*/
FLUTTER_EXPORT
@interface FlutterStandardMessageCodec : NSObject<FlutterMessageCodec>
@interface FlutterStandardMessageCodec : NSObject <FlutterMessageCodec>
+ (instancetype)codecWithReaderWriter:(FlutterStandardReaderWriter*)readerWriter;
@end
/**
......@@ -359,7 +410,7 @@ FLUTTER_EXPORT
those supported as top-level or leaf values by `FlutterJSONMessageCodec`.
*/
FLUTTER_EXPORT
@interface FlutterJSONMethodCodec : NSObject<FlutterMethodCodec>
@interface FlutterJSONMethodCodec : NSObject <FlutterMethodCodec>
@end
/**
......@@ -373,7 +424,8 @@ FLUTTER_EXPORT
`FlutterStandardMessageCodec`.
*/
FLUTTER_EXPORT
@interface FlutterStandardMethodCodec : NSObject<FlutterMethodCodec>
@interface FlutterStandardMethodCodec : NSObject <FlutterMethodCodec>
+ (instancetype)codecWithReaderWriter:(FlutterStandardReaderWriter*)readerWriter;
@end
NS_ASSUME_NONNULL_END
......
......@@ -6,20 +6,40 @@
#pragma mark - Codec for basic message channel
@implementation FlutterStandardMessageCodec
@implementation FlutterStandardMessageCodec {
FlutterStandardReaderWriter* _readerWriter;
}
+ (instancetype)sharedInstance {
static id _sharedInstance = nil;
if (!_sharedInstance) {
_sharedInstance = [FlutterStandardMessageCodec new];
FlutterStandardReaderWriter* readerWriter =
[[[FlutterStandardReaderWriter alloc] init] autorelease];
_sharedInstance = [[FlutterStandardMessageCodec alloc] initWithReaderWriter:readerWriter];
}
return _sharedInstance;
}
+ (instancetype)codecWithReaderWriter:(FlutterStandardReaderWriter*)readerWriter {
return [[[FlutterStandardMessageCodec alloc] initWithReaderWriter:readerWriter] autorelease];
}
- (instancetype)initWithReaderWriter:(FlutterStandardReaderWriter*)readerWriter {
self = [super init];
NSAssert(self, @"Super init cannot be nil");
_readerWriter = [readerWriter retain];
return self;
}
- (void)dealloc {
[_readerWriter release];
[super dealloc];
}
- (NSData*)encode:(id)message {
if (message == nil)
return nil;
NSMutableData* data = [NSMutableData dataWithCapacity:32];
FlutterStandardWriter* writer = [FlutterStandardWriter writerWithData:data];
FlutterStandardWriter* writer = [_readerWriter writerWithData:data];
[writer writeValue:message];
return data;
}
......@@ -27,7 +47,7 @@
- (id)decode:(NSData*)message {
if (message == nil)
return nil;
FlutterStandardReader* reader = [FlutterStandardReader readerWithData:message];
FlutterStandardReader* reader = [_readerWriter readerWithData:message];
id value = [reader readValue];
NSAssert(![reader hasMore], @"Corrupted standard message");
return value;
......@@ -36,18 +56,38 @@
#pragma mark - Codec for method channel
@implementation FlutterStandardMethodCodec
@implementation FlutterStandardMethodCodec {
FlutterStandardReaderWriter* _readerWriter;
}
+ (instancetype)sharedInstance {
static id _sharedInstance = nil;
if (!_sharedInstance) {
_sharedInstance = [FlutterStandardMethodCodec new];
FlutterStandardReaderWriter* readerWriter =
[[[FlutterStandardReaderWriter alloc] init] autorelease];
_sharedInstance = [[FlutterStandardMethodCodec alloc] initWithReaderWriter:readerWriter];
}
return _sharedInstance;
}
+ (instancetype)codecWithReaderWriter:(FlutterStandardReaderWriter*)readerWriter {
return [[[FlutterStandardMethodCodec alloc] initWithReaderWriter:readerWriter] autorelease];
}
- (instancetype)initWithReaderWriter:(FlutterStandardReaderWriter*)readerWriter {
self = [super init];
NSAssert(self, @"Super init cannot be nil");
_readerWriter = [readerWriter retain];
return self;
}
- (void)dealloc {
[_readerWriter release];
[super dealloc];
}
- (NSData*)encodeMethodCall:(FlutterMethodCall*)call {
NSMutableData* data = [NSMutableData dataWithCapacity:32];
FlutterStandardWriter* writer = [FlutterStandardWriter writerWithData:data];
FlutterStandardWriter* writer = [_readerWriter writerWithData:data];
[writer writeValue:call.method];
[writer writeValue:call.arguments];
return data;
......@@ -55,7 +95,7 @@
- (NSData*)encodeSuccessEnvelope:(id)result {
NSMutableData* data = [NSMutableData dataWithCapacity:32];
FlutterStandardWriter* writer = [FlutterStandardWriter writerWithData:data];
FlutterStandardWriter* writer = [_readerWriter writerWithData:data];
[writer writeByte:0];
[writer writeValue:result];
return data;
......@@ -63,7 +103,7 @@
- (NSData*)encodeErrorEnvelope:(FlutterError*)error {
NSMutableData* data = [NSMutableData dataWithCapacity:32];
FlutterStandardWriter* writer = [FlutterStandardWriter writerWithData:data];
FlutterStandardWriter* writer = [_readerWriter writerWithData:data];
[writer writeByte:1];
[writer writeValue:error.code];
[writer writeValue:error.message];
......@@ -72,7 +112,7 @@
}
- (FlutterMethodCall*)decodeMethodCall:(NSData*)message {
FlutterStandardReader* reader = [FlutterStandardReader readerWithData:message];
FlutterStandardReader* reader = [_readerWriter readerWithData:message];
id value1 = [reader readValue];
id value2 = [reader readValue];
NSAssert(![reader hasMore], @"Corrupted standard method call");
......@@ -81,7 +121,7 @@
}
- (id)decodeEnvelope:(NSData*)envelope {
FlutterStandardReader* reader = [FlutterStandardReader readerWithData:envelope];
FlutterStandardReader* reader = [_readerWriter readerWithData:envelope];
UInt8 flag = [reader readByte];
NSAssert(flag <= 1, @"Corrupted standard envelope");
id result;
......@@ -204,12 +244,6 @@ using namespace shell;
NSMutableData* _data;
}
+ (instancetype)writerWithData:(NSMutableData*)data {
FlutterStandardWriter* writer = [[FlutterStandardWriter alloc] initWithData:data];
[writer autorelease];
return writer;
}
- (instancetype)initWithData:(NSMutableData*)data {
self = [super init];
NSAssert(self, @"Super init cannot be nil");
......@@ -226,16 +260,24 @@ using namespace shell;
[_data appendBytes:&value length:1];
}
- (void)writeBytes:(const void*)bytes length:(NSUInteger)length {
[_data appendBytes:bytes length:length];
}
- (void)writeData:(NSData*)data {
[_data appendData:data];
}
- (void)writeSize:(UInt32)size {
if (size < 254) {
[self writeByte:(UInt8)size];
} else if (size <= 0xffff) {
[self writeByte:254];
UInt16 value = (UInt16)size;
[_data appendBytes:&value length:2];
[self writeBytes:&value length:2];
} else {
[self writeByte:255];
[_data appendBytes:&size length:4];
[self writeBytes:&size length:4];
}
}
......@@ -251,7 +293,7 @@ using namespace shell;
- (void)writeUTF8:(NSString*)value {
UInt32 length = [value lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
[self writeSize:length];
[_data appendBytes:value.UTF8String length:length];
[self writeBytes:value.UTF8String length:length];
}
- (void)writeValue:(id)value {
......@@ -269,17 +311,17 @@ using namespace shell;
strcmp(type, @encode(unsigned char)) == 0) {
SInt32 n = number.intValue;
[self writeByte:FlutterStandardFieldInt32];
[_data appendBytes:(UInt8*)&n length:4];
[self writeBytes:(UInt8*)&n length:4];
} else if (strcmp(type, @encode(signed long)) == 0 ||
strcmp(type, @encode(unsigned int)) == 0) {
SInt64 n = number.longValue;
[self writeByte:FlutterStandardFieldInt64];
[_data appendBytes:(UInt8*)&n length:8];
[self writeBytes:(UInt8*)&n length:8];
} else if (strcmp(type, @encode(double)) == 0 || strcmp(type, @encode(float)) == 0) {
Float64 f = number.doubleValue;
[self writeByte:FlutterStandardFieldFloat64];
[self writeAlignment:8];
[_data appendBytes:(UInt8*)&f length:8];
[self writeBytes:(UInt8*)&f length:8];
} else if (strcmp(type, @encode(unsigned long)) == 0 ||
strcmp(type, @encode(signed long long)) == 0 ||
strcmp(type, @encode(unsigned long long)) == 0) {
......@@ -303,7 +345,7 @@ using namespace shell;
[self writeByte:FlutterStandardFieldForDataType(typedData.type)];
[self writeSize:typedData.elementCount];
[self writeAlignment:typedData.elementSize];
[_data appendData:typedData.data];
[self writeData:typedData.data];
} else if ([value isKindOfClass:[NSArray class]]) {
NSArray* array = value;
[self writeByte:FlutterStandardFieldList];
......@@ -336,12 +378,6 @@ using namespace shell;
NSRange _range;
}
+ (instancetype)readerWithData:(NSData*)data {
FlutterStandardReader* reader = [[FlutterStandardReader alloc] initWithData:data];
[reader autorelease];
return reader;
}
- (instancetype)initWithData:(NSData*)data {
self = [super init];
NSAssert(self, @"Super init cannot be nil");
......@@ -359,7 +395,7 @@ using namespace shell;
return _range.location < _data.length;
}
- (void)readBytes:(void*)destination length:(int)length {
- (void)readBytes:(void*)destination length:(NSUInteger)length {
_range.length = length;
[_data getBytes:destination range:_range];
_range.location += _range.length;
......@@ -386,7 +422,7 @@ using namespace shell;
}
}
- (NSData*)readData:(int)length {
- (NSData*)readData:(NSUInteger)length {
_range.length = length;
NSData* data = [_data subdataWithRange:_range];
_range.location += _range.length;
......@@ -414,7 +450,11 @@ using namespace shell;
}
- (id)readValue {
FlutterStandardField field = (FlutterStandardField)[self readByte];
return [self readValueOfType:[self readByte]];
}
- (id)readValueOfType:(UInt8)type {
FlutterStandardField field = (FlutterStandardField)type;
switch (field) {
case FlutterStandardFieldNil:
return nil;
......@@ -472,4 +512,14 @@ using namespace shell;
}
}
@end
@implementation FlutterStandardReaderWriter
- (FlutterStandardWriter*)writerWithData:(NSMutableData*)data {
return [[[FlutterStandardWriter alloc] initWithData:data] autorelease];
}
- (FlutterStandardReader*)readerWithData:(NSData*)data {
return [[[FlutterStandardReader alloc] initWithData:data] autorelease];
}
@end
#pragma clang diagnostic pop
......@@ -21,7 +21,7 @@ typedef NS_ENUM(NSInteger, FlutterStandardField) {
FlutterStandardFieldInt64Data,
FlutterStandardFieldFloat64Data,
FlutterStandardFieldList,
FlutterStandardFieldMap
FlutterStandardFieldMap,
};
namespace shell {
......@@ -45,17 +45,4 @@ UInt8 elementSizeForFlutterStandardDataType(FlutterStandardDataType type) {
}
} // namespace shell
@interface FlutterStandardWriter : NSObject
+ (instancetype)writerWithData:(NSMutableData*)data;
- (void)writeByte:(UInt8)value;
- (void)writeValue:(id)value;
@end
@interface FlutterStandardReader : NSObject
+ (instancetype)readerWithData:(NSData*)data;
- (BOOL)hasMore;
- (UInt8)readByte;
- (id)readValue;
@end
#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERSTANDARDCODECINTERNAL_H_
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册