mirror of
https://github.com/moparisthebest/PhoneGap-SQLitePlugin-Android
synced 2024-11-14 13:05:12 -05:00
Original DroidGap version in Simple-DroidGap-test to work on issue #18
This commit is contained in:
parent
406017ce80
commit
67cd416c70
342
Simple-DroidGap-test/assets/www/SQLitePlugin.js
Normal file
342
Simple-DroidGap-test/assets/www/SQLitePlugin.js
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
/*
|
||||||
|
* PhoneGap is available under *either* the terms of the modified BSD license *or* the
|
||||||
|
* MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2005-2010, Nitobi Software Inc.
|
||||||
|
* Copyright (c) 2010-2011, IBM Corporation
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is purely for the Android 1.5/1.6 HTML 5 Storage
|
||||||
|
* I was hoping that Android 2.0 would deprecate this, but given the fact that
|
||||||
|
* most manufacturers ship with Android 1.5 and do not do OTA Updates, this is required
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// XXX TODO: use function() { ... } () to encapsulate these declarations (except for Java callback)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL result set object
|
||||||
|
* PRIVATE METHOD
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
var DDB_Rows = function() {
|
||||||
|
this.resultSet = []; // results array
|
||||||
|
this.length = 0; // number of rows
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get item from SQL result set
|
||||||
|
*
|
||||||
|
* @param row The row number to return
|
||||||
|
* @return The row object
|
||||||
|
*/
|
||||||
|
DDB_Rows.prototype.item = function(row) {
|
||||||
|
return this.resultSet[row];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL result set that is returned to user.
|
||||||
|
* PRIVATE METHOD
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
var DDB_Result = function() {
|
||||||
|
this.rows = new DDB_Rows();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage object that is called by native code when performing queries.
|
||||||
|
* PRIVATE METHOD
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
var DDB = function() {
|
||||||
|
this.queryQueue = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback from native code when query is complete.
|
||||||
|
* PRIVATE METHOD
|
||||||
|
*
|
||||||
|
* @param id Query id
|
||||||
|
*/
|
||||||
|
DDB.prototype.completeQuery = function(id, data) {
|
||||||
|
var query = this.queryQueue[id];
|
||||||
|
if (query) {
|
||||||
|
try {
|
||||||
|
delete this.queryQueue[id];
|
||||||
|
|
||||||
|
// Get transaction
|
||||||
|
var tx = query.tx;
|
||||||
|
|
||||||
|
// If transaction hasn't failed
|
||||||
|
// Note: We ignore all query results if previous query
|
||||||
|
// in the same transaction failed.
|
||||||
|
if (tx && tx.queryList[id]) {
|
||||||
|
|
||||||
|
// Save query results
|
||||||
|
var r = new DDB_Result();
|
||||||
|
r.rows.resultSet = data;
|
||||||
|
r.rows.length = data.length;
|
||||||
|
try {
|
||||||
|
if (typeof query.successCallback === 'function') {
|
||||||
|
query.successCallback(query.tx, r);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.log("executeSql error calling user success callback: "+ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.queryComplete(id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("executeSql error: "+e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback from native code when query fails
|
||||||
|
* PRIVATE METHOD
|
||||||
|
*
|
||||||
|
* @param reason Error message
|
||||||
|
* @param id Query id
|
||||||
|
*/
|
||||||
|
DDB.prototype.fail = function(reason, id) {
|
||||||
|
var query = this.queryQueue[id];
|
||||||
|
if (query) {
|
||||||
|
try {
|
||||||
|
delete this.queryQueue[id];
|
||||||
|
|
||||||
|
// Get transaction
|
||||||
|
var tx = query.tx;
|
||||||
|
|
||||||
|
// If transaction hasn't failed
|
||||||
|
// Note: We ignore all query results if previous query
|
||||||
|
// in the same transaction failed.
|
||||||
|
if (tx && tx.queryList[id]) {
|
||||||
|
tx.queryList = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof query.errorCallback === 'function') {
|
||||||
|
query.errorCallback(query.tx, reason);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
console.log("executeSql error calling user error callback: "+ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.queryFailed(id, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.log("executeSql error: "+e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var mycreateUUID = function() {
|
||||||
|
return myUUIDcreatePart(4) + '-' +
|
||||||
|
myUUIDcreatePart(2) + '-' +
|
||||||
|
myUUIDcreatePart(2) + '-' +
|
||||||
|
myUUIDcreatePart(2) + '-' +
|
||||||
|
myUUIDcreatePart(6);
|
||||||
|
};
|
||||||
|
|
||||||
|
myUUIDcreatePart = function(length) {
|
||||||
|
var uuidpart = "";
|
||||||
|
var i, uuidchar;
|
||||||
|
for (i=0; i<length; i++) {
|
||||||
|
uuidchar = parseInt((Math.random() * 256),0).toString(16);
|
||||||
|
if (uuidchar.length === 1) {
|
||||||
|
uuidchar = "0" + uuidchar;
|
||||||
|
}
|
||||||
|
uuidpart += uuidchar;
|
||||||
|
}
|
||||||
|
return uuidpart;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL query object
|
||||||
|
* PRIVATE METHOD
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param tx The transaction object that this query belongs to
|
||||||
|
*/
|
||||||
|
var DDB_Query = function(tx) {
|
||||||
|
|
||||||
|
// Set the id of the query
|
||||||
|
this.id = mycreateUUID();
|
||||||
|
|
||||||
|
// Add this query to the queue
|
||||||
|
dddb.queryQueue[this.id] = this;
|
||||||
|
|
||||||
|
// Init result
|
||||||
|
this.resultSet = [];
|
||||||
|
|
||||||
|
// Set transaction that this query belongs to
|
||||||
|
this.tx = tx;
|
||||||
|
|
||||||
|
// Add this query to transaction list
|
||||||
|
this.tx.queryList[this.id] = this;
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
this.successCallback = null;
|
||||||
|
this.errorCallback = null;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transaction object
|
||||||
|
* PRIVATE METHOD
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
var DDB_Tx = function() {
|
||||||
|
|
||||||
|
// Set the id of the transaction
|
||||||
|
this.id = mycreateUUID();
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
this.successCallback = null;
|
||||||
|
this.errorCallback = null;
|
||||||
|
|
||||||
|
// Query list
|
||||||
|
this.queryList = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark query in transaction as complete.
|
||||||
|
* If all queries are complete, call the user's transaction success callback.
|
||||||
|
*
|
||||||
|
* @param id Query id
|
||||||
|
*/
|
||||||
|
DDB_Tx.prototype.queryComplete = function(id) {
|
||||||
|
delete this.queryList[id];
|
||||||
|
|
||||||
|
// If no more outstanding queries, then fire transaction success
|
||||||
|
if (this.successCallback) {
|
||||||
|
var count = 0;
|
||||||
|
var i;
|
||||||
|
for (i in this.queryList) {
|
||||||
|
if (this.queryList.hasOwnProperty(i)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count === 0) {
|
||||||
|
try {
|
||||||
|
this.successCallback();
|
||||||
|
} catch(e) {
|
||||||
|
console.log("Transaction error calling user success callback: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark query in transaction as failed.
|
||||||
|
*
|
||||||
|
* @param id Query id
|
||||||
|
* @param reason Error message
|
||||||
|
*/
|
||||||
|
DDB_Tx.prototype.queryFailed = function(id, reason) {
|
||||||
|
|
||||||
|
// The sql queries in this transaction have already been run, since
|
||||||
|
// we really don't have a real transaction implemented in native code.
|
||||||
|
// However, the user callbacks for the remaining sql queries in transaction
|
||||||
|
// will not be called.
|
||||||
|
this.queryList = {};
|
||||||
|
|
||||||
|
if (this.errorCallback) {
|
||||||
|
try {
|
||||||
|
this.errorCallback(reason);
|
||||||
|
} catch(e) {
|
||||||
|
console.log("Transaction error calling user error callback: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute SQL statement
|
||||||
|
*
|
||||||
|
* @param sql SQL statement to execute
|
||||||
|
* @param params Statement parameters
|
||||||
|
* @param successCallback Success callback
|
||||||
|
* @param errorCallback Error callback
|
||||||
|
*/
|
||||||
|
DDB_Tx.prototype.executeSql = function(sql, params, successCallback, errorCallback) {
|
||||||
|
|
||||||
|
// Init params array
|
||||||
|
if (typeof params === 'undefined') {
|
||||||
|
params = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create query and add to queue
|
||||||
|
var query = new DDB_Query(this);
|
||||||
|
dddb.queryQueue[query.id] = query;
|
||||||
|
|
||||||
|
// Save callbacks
|
||||||
|
query.successCallback = successCallback;
|
||||||
|
query.errorCallback = errorCallback;
|
||||||
|
|
||||||
|
// Call native code
|
||||||
|
PhoneGap.exec(null, null, "SQLitePlugin", "executeSql", [sql, params, query.id]);
|
||||||
|
};
|
||||||
|
|
||||||
|
var DatabaseShell = function() {
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a transaction.
|
||||||
|
* Does not support rollback in event of failure.
|
||||||
|
*
|
||||||
|
* @param process {Function} The transaction function
|
||||||
|
* @param successCallback {Function}
|
||||||
|
* @param errorCallback {Function}
|
||||||
|
*/
|
||||||
|
DatabaseShell.prototype.transaction = function(process, errorCallback, successCallback) {
|
||||||
|
var tx = new DDB_Tx();
|
||||||
|
tx.successCallback = successCallback;
|
||||||
|
tx.errorCallback = errorCallback;
|
||||||
|
try {
|
||||||
|
process(tx);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Transaction error: "+e);
|
||||||
|
if (tx.errorCallback) {
|
||||||
|
try {
|
||||||
|
tx.errorCallback(e);
|
||||||
|
} catch (ex) {
|
||||||
|
console.log("Transaction error calling user error callback: "+e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open database
|
||||||
|
*
|
||||||
|
* @param name Database name
|
||||||
|
* @param version Database version
|
||||||
|
* @param display_name Database display name
|
||||||
|
* @param size Database size in bytes
|
||||||
|
* @return Database object
|
||||||
|
*/
|
||||||
|
var DDB_openDatabase = function(name, version, display_name, size) {
|
||||||
|
PhoneGap.exec(null, null, "SQLitePlugin", "openDatabase", [name, version, display_name, size]);
|
||||||
|
var db = new DatabaseShell();
|
||||||
|
return db;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For browsers with no localStorage we emulate it with SQLite. Follows the w3c api.
|
||||||
|
* TODO: Do similar for sessionStorage.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
|
||||||
|
window.sqlitePlugin = {
|
||||||
|
openDatabase: function(name, version, desc, size) {
|
||||||
|
window.dddb = new DDB();
|
||||||
|
return DDB_openDatabase(name, version, desc, size);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
* PhoneGap is available under *either* the terms of the modified BSD license *or* the
|
||||||
|
* MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2005-2010, Nitobi Software Inc.
|
||||||
|
* Copyright (c) 2010, IBM Corporation
|
||||||
|
*/
|
||||||
|
package com.phonegap.plugin.sqlitePlugin;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import com.phonegap.api.Plugin;
|
||||||
|
import com.phonegap.api.PluginResult;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.*;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class implements the HTML5 database support for Android 1.X devices. It
|
||||||
|
* is not used for Android 2.X, since HTML5 database is built in to the browser.
|
||||||
|
*/
|
||||||
|
public class SQLitePlugin extends Plugin {
|
||||||
|
|
||||||
|
// Data Definition Language
|
||||||
|
private static final String ALTER = "alter";
|
||||||
|
private static final String CREATE = "create";
|
||||||
|
private static final String DROP = "drop";
|
||||||
|
private static final String TRUNCATE = "truncate";
|
||||||
|
|
||||||
|
SQLiteDatabase myDb = null; // Database object
|
||||||
|
String path = null; // Database path
|
||||||
|
String dbName = null; // Database name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public SQLitePlugin() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the request and returns PluginResult.
|
||||||
|
*
|
||||||
|
* @param action
|
||||||
|
* The action to execute.
|
||||||
|
* @param args
|
||||||
|
* JSONArry of arguments for the plugin.
|
||||||
|
* @param callbackId
|
||||||
|
* The callback id used when calling back into JavaScript.
|
||||||
|
* @return A PluginResult object with a status and message.
|
||||||
|
*/
|
||||||
|
public PluginResult execute(String action, JSONArray args, String callbackId) {
|
||||||
|
PluginResult.Status status = PluginResult.Status.OK;
|
||||||
|
String result = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: Do we want to allow a user to do this, since they could get
|
||||||
|
// to other app databases?
|
||||||
|
if (action.equals("setStorage")) {
|
||||||
|
this.setStorage(args.getString(0));
|
||||||
|
} else if (action.equals("openDatabase")) {
|
||||||
|
this.openDatabase(args.getString(0), args.getString(1),
|
||||||
|
args.getString(2), args.getLong(3));
|
||||||
|
} else if (action.equals("executeSql")) {
|
||||||
|
String[] s = null;
|
||||||
|
if (args.isNull(1)) {
|
||||||
|
s = new String[0];
|
||||||
|
} else {
|
||||||
|
JSONArray a = args.getJSONArray(1);
|
||||||
|
int len = a.length();
|
||||||
|
s = new String[len];
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
s[i] = a.getString(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.executeSql(args.getString(0), s, args.getString(2));
|
||||||
|
}
|
||||||
|
return new PluginResult(status, result);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
return new PluginResult(PluginResult.Status.JSON_EXCEPTION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifies if action to be executed returns a value and should be run
|
||||||
|
* synchronously.
|
||||||
|
*
|
||||||
|
* @param action
|
||||||
|
* The action to execute
|
||||||
|
* @return T=returns value
|
||||||
|
*/
|
||||||
|
public boolean isSynch(String action) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up and close database.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
if (this.myDb != null) {
|
||||||
|
this.myDb.close();
|
||||||
|
this.myDb = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// LOCAL METHODS
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the application package for the database. Each application saves its
|
||||||
|
* database files in a directory with the application package as part of the
|
||||||
|
* file name.
|
||||||
|
*
|
||||||
|
* For example, application "com.phonegap.demo.Demo" would save its database
|
||||||
|
* files in "/data/data/com.phonegap.demo/databases/" directory.
|
||||||
|
*
|
||||||
|
* @param appPackage
|
||||||
|
* The application package.
|
||||||
|
*/
|
||||||
|
public void setStorage(String appPackage) {
|
||||||
|
this.path = "/data/data/" + appPackage + "/databases/";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open database.
|
||||||
|
*
|
||||||
|
* @param db
|
||||||
|
* The name of the database
|
||||||
|
* @param version
|
||||||
|
* The version
|
||||||
|
* @param display_name
|
||||||
|
* The display name
|
||||||
|
* @param size
|
||||||
|
* The size in bytes
|
||||||
|
*/
|
||||||
|
public void openDatabase(String db, String version, String display_name,
|
||||||
|
long size) {
|
||||||
|
|
||||||
|
// If database is open, then close it
|
||||||
|
if (this.myDb != null) {
|
||||||
|
this.myDb.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no database path, generate from application package
|
||||||
|
if (this.path == null) {
|
||||||
|
Package pack = this.ctx.getClass().getPackage();
|
||||||
|
String appPackage = pack.getName();
|
||||||
|
this.setStorage(appPackage);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dbName = this.path + db + ".db";
|
||||||
|
this.myDb = SQLiteDatabase.openOrCreateDatabase(this.dbName, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute SQL statement.
|
||||||
|
*
|
||||||
|
* @param query
|
||||||
|
* The SQL query
|
||||||
|
* @param params
|
||||||
|
* Parameters for the query
|
||||||
|
* @param tx_id
|
||||||
|
* Transaction id
|
||||||
|
*/
|
||||||
|
public void executeSql(String query, String[] params, String tx_id) {
|
||||||
|
try {
|
||||||
|
if (isDDL(query)) {
|
||||||
|
this.myDb.execSQL(query);
|
||||||
|
this.sendJavascript("dddb.completeQuery('" + tx_id + "', '');");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Cursor myCursor = this.myDb.rawQuery(query, params);
|
||||||
|
this.processResults(myCursor, tx_id);
|
||||||
|
myCursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (SQLiteException ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
|
System.out.println("SQLitePlugin.executeSql(): Error=" + ex.getMessage());
|
||||||
|
|
||||||
|
// Send error message back to JavaScript
|
||||||
|
this.sendJavascript("dddb.fail('" + ex.getMessage() + "','" + tx_id + "');");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see the the query is a Data Definintion command
|
||||||
|
*
|
||||||
|
* @param query to be executed
|
||||||
|
* @return true if it is a DDL command, false otherwise
|
||||||
|
*/
|
||||||
|
private boolean isDDL(String query) {
|
||||||
|
String cmd = query.toLowerCase();
|
||||||
|
if (cmd.startsWith(DROP) || cmd.startsWith(CREATE) || cmd.startsWith(ALTER) || cmd.startsWith(TRUNCATE)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process query results.
|
||||||
|
*
|
||||||
|
* @param cur
|
||||||
|
* Cursor into query results
|
||||||
|
* @param tx_id
|
||||||
|
* Transaction id
|
||||||
|
*/
|
||||||
|
public void processResults(Cursor cur, String tx_id) {
|
||||||
|
|
||||||
|
String result = "[]";
|
||||||
|
// If query result has rows
|
||||||
|
|
||||||
|
if (cur.moveToFirst()) {
|
||||||
|
JSONArray fullresult = new JSONArray();
|
||||||
|
String key = "";
|
||||||
|
String value = "";
|
||||||
|
int colCount = cur.getColumnCount();
|
||||||
|
|
||||||
|
// Build up JSON result object for each row
|
||||||
|
do {
|
||||||
|
JSONObject row = new JSONObject();
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < colCount; ++i) {
|
||||||
|
key = cur.getColumnName(i);
|
||||||
|
value = cur.getString(i);
|
||||||
|
row.put(key, value);
|
||||||
|
}
|
||||||
|
fullresult.put(row);
|
||||||
|
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
} while (cur.moveToNext());
|
||||||
|
|
||||||
|
result = fullresult.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let JavaScript know that there are no more rows
|
||||||
|
this.sendJavascript("dddb.completeQuery('" + tx_id + "', " + result
|
||||||
|
+ ");");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user