Merge change 5459 into donut
* changes: Make the BackupHelperDispatcher properly handle multiple helpers.
This commit is contained in:
@@ -67,7 +67,7 @@ public abstract class BackupAgent extends ContextWrapper {
|
|||||||
* here after writing the requested data to dataFd.
|
* here after writing the requested data to dataFd.
|
||||||
*/
|
*/
|
||||||
public abstract void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
public abstract void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
||||||
ParcelFileDescriptor newState);
|
ParcelFileDescriptor newState) throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The application is being restored from backup, and should replace any
|
* The application is being restored from backup, and should replace any
|
||||||
@@ -120,6 +120,9 @@ public abstract class BackupAgent extends ContextWrapper {
|
|||||||
BackupDataOutput output = new BackupDataOutput(data.getFileDescriptor());
|
BackupDataOutput output = new BackupDataOutput(data.getFileDescriptor());
|
||||||
try {
|
try {
|
||||||
BackupAgent.this.onBackup(oldState, output, newState);
|
BackupAgent.this.onBackup(oldState, output, newState);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
|
||||||
|
throw new RuntimeException(ex);
|
||||||
} catch (RuntimeException ex) {
|
} catch (RuntimeException ex) {
|
||||||
Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
|
Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex);
|
||||||
throw ex;
|
throw ex;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class BackupHelperAgent extends BackupAgent {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
||||||
ParcelFileDescriptor newState) {
|
ParcelFileDescriptor newState) throws IOException {
|
||||||
mDispatcher.performBackup(oldState, data, newState);
|
mDispatcher.performBackup(oldState, data, newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ package android.backup;
|
|||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.FileDescriptor;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -27,6 +30,11 @@ import java.util.Map;
|
|||||||
public class BackupHelperDispatcher {
|
public class BackupHelperDispatcher {
|
||||||
private static final String TAG = "BackupHelperDispatcher";
|
private static final String TAG = "BackupHelperDispatcher";
|
||||||
|
|
||||||
|
private static class Header {
|
||||||
|
int chunkSize; // not including the header
|
||||||
|
String keyPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
TreeMap<String,BackupHelper> mHelpers = new TreeMap<String,BackupHelper>();
|
TreeMap<String,BackupHelper> mHelpers = new TreeMap<String,BackupHelper>();
|
||||||
|
|
||||||
public BackupHelperDispatcher() {
|
public BackupHelperDispatcher() {
|
||||||
@@ -36,13 +44,63 @@ public class BackupHelperDispatcher {
|
|||||||
mHelpers.put(keyPrefix, helper);
|
mHelpers.put(keyPrefix, helper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** TODO: Make this save and restore the key prefix. */
|
|
||||||
public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
||||||
ParcelFileDescriptor newState) {
|
ParcelFileDescriptor newState) throws IOException {
|
||||||
// Write out the state files -- mHelpers is a TreeMap, so the order is well defined.
|
// First, do the helpers that we've already done, since they're already in the state
|
||||||
for (Map.Entry<String,BackupHelper> entry: mHelpers.entrySet()) {
|
// file.
|
||||||
data.setKeyPrefix(entry.getKey());
|
int err;
|
||||||
entry.getValue().performBackup(oldState, data, newState);
|
Header header = new Header();
|
||||||
|
TreeMap<String,BackupHelper> helpers = (TreeMap<String,BackupHelper>)mHelpers.clone();
|
||||||
|
FileDescriptor oldStateFD = null;
|
||||||
|
FileDescriptor newStateFD = newState.getFileDescriptor();
|
||||||
|
|
||||||
|
if (oldState != null) {
|
||||||
|
oldStateFD = oldState.getFileDescriptor();
|
||||||
|
while ((err = readHeader_native(header, oldStateFD)) >= 0) {
|
||||||
|
if (err == 0) {
|
||||||
|
BackupHelper helper = helpers.get(header.keyPrefix);
|
||||||
|
Log.d(TAG, "handling existing helper '" + header.keyPrefix + "' " + helper);
|
||||||
|
if (helper != null) {
|
||||||
|
doOneBackup(oldState, data, newState, header, helper);
|
||||||
|
helpers.remove(header.keyPrefix);
|
||||||
|
} else {
|
||||||
|
skipChunk_native(oldStateFD, header.chunkSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then go through and do the rest that we haven't done.
|
||||||
|
for (Map.Entry<String,BackupHelper> entry: helpers.entrySet()) {
|
||||||
|
header.keyPrefix = entry.getKey();
|
||||||
|
Log.d(TAG, "handling new helper '" + header.keyPrefix + "'");
|
||||||
|
BackupHelper helper = entry.getValue();
|
||||||
|
doOneBackup(oldState, data, newState, header, helper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doOneBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
||||||
|
ParcelFileDescriptor newState, Header header, BackupHelper helper)
|
||||||
|
throws IOException {
|
||||||
|
int err;
|
||||||
|
FileDescriptor newStateFD = newState.getFileDescriptor();
|
||||||
|
|
||||||
|
// allocate space for the header in the file
|
||||||
|
int pos = allocateHeader_native(header, newStateFD);
|
||||||
|
if (pos < 0) {
|
||||||
|
throw new IOException("allocateHeader_native failed (error " + pos + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
data.setKeyPrefix(header.keyPrefix);
|
||||||
|
|
||||||
|
// do the backup
|
||||||
|
helper.performBackup(oldState, data, newState);
|
||||||
|
|
||||||
|
// fill in the header (seeking back to pos). The file pointer will be returned to
|
||||||
|
// where it was at the end of performBackup. Header.chunkSize will not be filled in.
|
||||||
|
err = writeHeader_native(header, newStateFD, pos);
|
||||||
|
if (err != 0) {
|
||||||
|
throw new IOException("writeHeader_native failed (error " + err + ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,5 +141,11 @@ public class BackupHelperDispatcher {
|
|||||||
helper.writeRestoreSnapshot(newState);
|
helper.writeRestoreSnapshot(newState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static native int readHeader_native(Header h, FileDescriptor fd);
|
||||||
|
private static native int skipChunk_native(FileDescriptor fd, int bytesToSkip);
|
||||||
|
|
||||||
|
private static native int allocateHeader_native(Header h, FileDescriptor fd);
|
||||||
|
private static native int writeHeader_native(Header h, FileDescriptor fd, int pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ LOCAL_SRC_FILES:= \
|
|||||||
com_android_internal_graphics_NativeUtils.cpp \
|
com_android_internal_graphics_NativeUtils.cpp \
|
||||||
android_backup_BackupDataInput.cpp \
|
android_backup_BackupDataInput.cpp \
|
||||||
android_backup_BackupDataOutput.cpp \
|
android_backup_BackupDataOutput.cpp \
|
||||||
android_backup_FileBackupHelperBase.cpp
|
android_backup_FileBackupHelperBase.cpp \
|
||||||
|
android_backup_BackupHelperDispatcher.cpp
|
||||||
|
|
||||||
LOCAL_C_INCLUDES += \
|
LOCAL_C_INCLUDES += \
|
||||||
$(JNI_H_INCLUDE) \
|
$(JNI_H_INCLUDE) \
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ extern int register_android_location_GpsLocationProvider(JNIEnv* env);
|
|||||||
extern int register_android_backup_BackupDataInput(JNIEnv *env);
|
extern int register_android_backup_BackupDataInput(JNIEnv *env);
|
||||||
extern int register_android_backup_BackupDataOutput(JNIEnv *env);
|
extern int register_android_backup_BackupDataOutput(JNIEnv *env);
|
||||||
extern int register_android_backup_FileBackupHelperBase(JNIEnv *env);
|
extern int register_android_backup_FileBackupHelperBase(JNIEnv *env);
|
||||||
|
extern int register_android_backup_BackupHelperDispatcher(JNIEnv *env);
|
||||||
|
|
||||||
static AndroidRuntime* gCurRuntime = NULL;
|
static AndroidRuntime* gCurRuntime = NULL;
|
||||||
|
|
||||||
@@ -1131,6 +1132,7 @@ static const RegJNIRec gRegJNI[] = {
|
|||||||
REG_JNI(register_android_backup_BackupDataInput),
|
REG_JNI(register_android_backup_BackupDataInput),
|
||||||
REG_JNI(register_android_backup_BackupDataOutput),
|
REG_JNI(register_android_backup_BackupDataOutput),
|
||||||
REG_JNI(register_android_backup_FileBackupHelperBase),
|
REG_JNI(register_android_backup_FileBackupHelperBase),
|
||||||
|
REG_JNI(register_android_backup_BackupHelperDispatcher),
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
251
core/jni/android_backup_BackupHelperDispatcher.cpp
Normal file
251
core/jni/android_backup_BackupHelperDispatcher.cpp
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define LOG_TAG "BackupHelperDispatcher_native"
|
||||||
|
#include <utils/Log.h>
|
||||||
|
|
||||||
|
#include "JNIHelp.h"
|
||||||
|
#include <android_runtime/AndroidRuntime.h>
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/uio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
|
||||||
|
#define VERSION_1_HEADER 0x01706c48 // 'Hlp'1 little endian
|
||||||
|
|
||||||
|
namespace android
|
||||||
|
{
|
||||||
|
|
||||||
|
struct chunk_header_v1 {
|
||||||
|
int headerSize;
|
||||||
|
int version;
|
||||||
|
int dataSize; // corresponds to Header.chunkSize
|
||||||
|
int nameLength; // not including the NULL terminator, which is not written to the file
|
||||||
|
};
|
||||||
|
|
||||||
|
// java.io.FileDescriptor
|
||||||
|
static jfieldID s_descriptorField = 0;
|
||||||
|
static jfieldID s_chunkSizeField = 0;
|
||||||
|
static jfieldID s_keyPrefixField = 0;
|
||||||
|
|
||||||
|
static int
|
||||||
|
readHeader_native(JNIEnv* env, jobject clazz, jobject headerObj, jobject fdObj)
|
||||||
|
{
|
||||||
|
chunk_header_v1 flattenedHeader;
|
||||||
|
int fd;
|
||||||
|
ssize_t amt;
|
||||||
|
String8 keyPrefix;
|
||||||
|
char* buf;
|
||||||
|
|
||||||
|
fd = env->GetIntField(fdObj, s_descriptorField);
|
||||||
|
|
||||||
|
amt = read(fd, &flattenedHeader.headerSize, sizeof(flattenedHeader.headerSize));
|
||||||
|
if (amt != sizeof(flattenedHeader.headerSize)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int remainingHeader = flattenedHeader.headerSize - sizeof(flattenedHeader.headerSize);
|
||||||
|
|
||||||
|
if (flattenedHeader.headerSize < (int)sizeof(chunk_header_v1)) {
|
||||||
|
LOGW("Skipping unknown header: %d bytes", flattenedHeader.headerSize);
|
||||||
|
if (remainingHeader > 0) {
|
||||||
|
lseek(fd, remainingHeader, SEEK_CUR);
|
||||||
|
// >0 means skip this chunk
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
amt = read(fd, &flattenedHeader.version,
|
||||||
|
sizeof(chunk_header_v1)-sizeof(flattenedHeader.headerSize));
|
||||||
|
if (amt <= 0) {
|
||||||
|
LOGW("Failed reading chunk header");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
remainingHeader -= sizeof(chunk_header_v1)-sizeof(flattenedHeader.headerSize);
|
||||||
|
|
||||||
|
if (flattenedHeader.version != VERSION_1_HEADER) {
|
||||||
|
LOGW("Skipping unknown header version: 0x%08x, %d bytes", flattenedHeader.version,
|
||||||
|
flattenedHeader.headerSize);
|
||||||
|
if (remainingHeader > 0) {
|
||||||
|
lseek(fd, remainingHeader, SEEK_CUR);
|
||||||
|
// >0 means skip this chunk
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flattenedHeader.dataSize < 0 || flattenedHeader.nameLength < 0 ||
|
||||||
|
remainingHeader < flattenedHeader.nameLength) {
|
||||||
|
LOGW("Malformed V1 header remainingHeader=%d dataSize=%d nameLength=%d", remainingHeader,
|
||||||
|
flattenedHeader.dataSize, flattenedHeader.nameLength);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = keyPrefix.lockBuffer(flattenedHeader.nameLength);
|
||||||
|
if (buf == NULL) {
|
||||||
|
LOGW("unable to allocate %d bytes", flattenedHeader.nameLength);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
amt = read(fd, buf, flattenedHeader.nameLength);
|
||||||
|
|
||||||
|
keyPrefix.unlockBuffer(flattenedHeader.nameLength);
|
||||||
|
|
||||||
|
remainingHeader -= flattenedHeader.nameLength;
|
||||||
|
|
||||||
|
LOGD("remainingHeader=%d", remainingHeader);
|
||||||
|
|
||||||
|
if (remainingHeader > 0) {
|
||||||
|
lseek(fd, remainingHeader, SEEK_CUR);
|
||||||
|
}
|
||||||
|
|
||||||
|
env->SetIntField(headerObj, s_chunkSizeField, flattenedHeader.dataSize);
|
||||||
|
env->SetObjectField(headerObj, s_keyPrefixField, env->NewStringUTF(keyPrefix.string()));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
skipChunk_native(JNIEnv* env, jobject clazz, jobject fdObj, jint bytesToSkip)
|
||||||
|
{
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
fd = env->GetIntField(fdObj, s_descriptorField);
|
||||||
|
|
||||||
|
lseek(fd, bytesToSkip, SEEK_CUR);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
padding_len(int len)
|
||||||
|
{
|
||||||
|
len = len % 4;
|
||||||
|
return len == 0 ? len : 4 - len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
allocateHeader_native(JNIEnv* env, jobject clazz, jobject headerObj, jobject fdObj)
|
||||||
|
{
|
||||||
|
int pos;
|
||||||
|
jstring nameObj;
|
||||||
|
int nameLength;
|
||||||
|
int namePadding;
|
||||||
|
int headerSize;
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
fd = env->GetIntField(fdObj, s_descriptorField);
|
||||||
|
|
||||||
|
nameObj = (jstring)env->GetObjectField(headerObj, s_keyPrefixField);
|
||||||
|
|
||||||
|
nameLength = env->GetStringUTFLength(nameObj);
|
||||||
|
namePadding = padding_len(nameLength);
|
||||||
|
|
||||||
|
headerSize = sizeof(chunk_header_v1) + nameLength + namePadding;
|
||||||
|
|
||||||
|
pos = lseek(fd, 0, SEEK_CUR);
|
||||||
|
|
||||||
|
lseek(fd, headerSize, SEEK_CUR);
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
writeHeader_native(JNIEnv* env, jobject clazz, jobject headerObj, jobject fdObj, jint pos)
|
||||||
|
{
|
||||||
|
int err;
|
||||||
|
chunk_header_v1 header;
|
||||||
|
int fd;
|
||||||
|
int namePadding;
|
||||||
|
int prevPos;
|
||||||
|
jstring nameObj;
|
||||||
|
const char* buf;
|
||||||
|
|
||||||
|
fd = env->GetIntField(fdObj, s_descriptorField);
|
||||||
|
prevPos = lseek(fd, 0, SEEK_CUR);
|
||||||
|
|
||||||
|
nameObj = (jstring)env->GetObjectField(headerObj, s_keyPrefixField);
|
||||||
|
header.nameLength = env->GetStringUTFLength(nameObj);
|
||||||
|
namePadding = padding_len(header.nameLength);
|
||||||
|
|
||||||
|
header.headerSize = sizeof(chunk_header_v1) + header.nameLength + namePadding;
|
||||||
|
header.version = VERSION_1_HEADER;
|
||||||
|
|
||||||
|
lseek(fd, pos, SEEK_SET);
|
||||||
|
err = write(fd, &header, sizeof(chunk_header_v1));
|
||||||
|
if (err != sizeof(chunk_header_v1)) {
|
||||||
|
return errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = env->GetStringUTFChars(nameObj, NULL);
|
||||||
|
err = write(fd, buf, header.nameLength);
|
||||||
|
env->ReleaseStringUTFChars(nameObj, buf);
|
||||||
|
if (err != header.nameLength) {
|
||||||
|
return errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (namePadding != 0) {
|
||||||
|
int zero = 0;
|
||||||
|
err = write(fd, &zero, namePadding);
|
||||||
|
if (err != namePadding) {
|
||||||
|
return errno;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lseek(fd, prevPos, SEEK_SET);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const JNINativeMethod g_methods[] = {
|
||||||
|
{ "readHeader_native",
|
||||||
|
"(Landroid/backup/BackupHelperDispatcher$Header;Ljava/io/FileDescriptor;)I",
|
||||||
|
(void*)readHeader_native },
|
||||||
|
{ "skipChunk_native",
|
||||||
|
"(Ljava/io/FileDescriptor;I)I",
|
||||||
|
(void*)skipChunk_native },
|
||||||
|
{ "allocateHeader_native",
|
||||||
|
"(Landroid/backup/BackupHelperDispatcher$Header;Ljava/io/FileDescriptor;)I",
|
||||||
|
(void*)allocateHeader_native },
|
||||||
|
{ "writeHeader_native",
|
||||||
|
"(Landroid/backup/BackupHelperDispatcher$Header;Ljava/io/FileDescriptor;I)I",
|
||||||
|
(void*)writeHeader_native },
|
||||||
|
};
|
||||||
|
|
||||||
|
int register_android_backup_BackupHelperDispatcher(JNIEnv* env)
|
||||||
|
{
|
||||||
|
jclass clazz;
|
||||||
|
|
||||||
|
clazz = env->FindClass("java/io/FileDescriptor");
|
||||||
|
LOG_FATAL_IF(clazz == NULL, "Unable to find class java.io.FileDescriptor");
|
||||||
|
s_descriptorField = env->GetFieldID(clazz, "descriptor", "I");
|
||||||
|
LOG_FATAL_IF(s_descriptorField == NULL,
|
||||||
|
"Unable to find descriptor field in java.io.FileDescriptor");
|
||||||
|
|
||||||
|
clazz = env->FindClass("android/backup/BackupHelperDispatcher$Header");
|
||||||
|
LOG_FATAL_IF(clazz == NULL,
|
||||||
|
"Unable to find class android.backup.BackupHelperDispatcher.Header");
|
||||||
|
s_chunkSizeField = env->GetFieldID(clazz, "chunkSize", "I");
|
||||||
|
LOG_FATAL_IF(s_chunkSizeField == NULL,
|
||||||
|
"Unable to find chunkSize field in android.backup.BackupHelperDispatcher.Header");
|
||||||
|
s_keyPrefixField = env->GetFieldID(clazz, "keyPrefix", "Ljava/lang/String;");
|
||||||
|
LOG_FATAL_IF(s_keyPrefixField == NULL,
|
||||||
|
"Unable to find keyPrefix field in android.backup.BackupHelperDispatcher.Header");
|
||||||
|
|
||||||
|
return AndroidRuntime::registerNativeMethods(env, "android/backup/BackupHelperDispatcher",
|
||||||
|
g_methods, NELEM(g_methods));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ public class BackupTestAgent extends BackupHelperAgent
|
|||||||
{
|
{
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
addHelper("data_files", new FileBackupHelper(this, BackupTestActivity.FILE_NAME));
|
addHelper("data_files", new FileBackupHelper(this, BackupTestActivity.FILE_NAME));
|
||||||
|
addHelper("more_data_files", new FileBackupHelper(this, "another_file.txt"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user