mirror of
https://gitee.com/houstudio/Cdroid.git
synced 2024-12-03 12:48:56 +08:00
fix velocitytracker,noe numberpicker can fling happy:)
This commit is contained in:
parent
784e675587
commit
3d4c621089
@ -76,4 +76,4 @@ if (FONTCONFIG_LIBRARIES AND FONTCONFIG_INCLUDE_DIRS)
|
|||||||
set(FONTCONFIG_INCLUDE_DIR ${FONTCONFIG_INCLUDE_DIRS})
|
set(FONTCONFIG_INCLUDE_DIR ${FONTCONFIG_INCLUDE_DIRS})
|
||||||
mark_as_advanced(FONTCONFIG_LIBRARIES FONTCONFIG_LIBRARY FONTCONFIG_INCLUDE_DIR FONTCONFIG_INCLUDE_DIRS)
|
mark_as_advanced(FONTCONFIG_LIBRARIES FONTCONFIG_LIBRARY FONTCONFIG_INCLUDE_DIR FONTCONFIG_INCLUDE_DIRS)
|
||||||
|
|
||||||
endif (FONTCONFIG_LIBRARIES AND FONTCONFIG_INCLUDE_DIR)
|
endif()
|
||||||
|
@ -46,7 +46,7 @@ InputDevice::InputDevice(int fdev):listener(nullptr){
|
|||||||
const INPUTAXISINFO*axis=devInfos.axis+j;
|
const INPUTAXISINFO*axis=devInfos.axis+j;
|
||||||
if(axis->maximum!=axis->minimum)
|
if(axis->maximum!=axis->minimum)
|
||||||
mDeviceInfo.addMotionRange(axis->axis,0/*source*/,axis->minimum,axis->maximum,axis->flat,axis->fuzz,axis->resolution);
|
mDeviceInfo.addMotionRange(axis->axis,0/*source*/,axis->minimum,axis->maximum,axis->flat,axis->fuzz,axis->resolution);
|
||||||
LOGI_IF(axis->maximum!=axis->minimum,"axis[%d] range=%d,%d",axis->axis,axis->minimum,axis->maximum);
|
LOGI_IF(axis->maximum!=axis->minimum,"devfd=%d axis[%d] range=%d,%d",fdev,axis->axis,axis->minimum,axis->maximum);
|
||||||
}
|
}
|
||||||
|
|
||||||
// See if this is a keyboard. Ignore everything in the button range except for
|
// See if this is a keyboard. Ignore everything in the button range except for
|
||||||
@ -213,7 +213,7 @@ int KeyDevice::putRawEvent(const struct timeval&tv,int type,int code,int value){
|
|||||||
mRepeatCount=0;
|
mRepeatCount=0;
|
||||||
|
|
||||||
mEvent.initialize(getId(),getSource(),(value?KeyEvent::ACTION_DOWN:KeyEvent::ACTION_UP)/*action*/,flags,
|
mEvent.initialize(getId(),getSource(),(value?KeyEvent::ACTION_DOWN:KeyEvent::ACTION_UP)/*action*/,flags,
|
||||||
keycode,code/*scancode*/,0/*metaState*/,mRepeatCount, mDownTime,SystemClock::uptimeNanos()/*eventtime*/);
|
keycode,code/*scancode*/,0/*metaState*/,mRepeatCount, mDownTime,SystemClock::uptimeMicros()/*eventtime*/);
|
||||||
LOGV("fd[%d] keycode:%08x->%04x[%s] action=%d flags=%d",getId(),code,keycode, mEvent.getLabel(),value,flags);
|
LOGV("fd[%d] keycode:%08x->%04x[%s] action=%d flags=%d",getId(),code,keycode, mEvent.getLabel(),value,flags);
|
||||||
if(listener)listener(mEvent);
|
if(listener)listener(mEvent);
|
||||||
break;
|
break;
|
||||||
@ -309,10 +309,10 @@ int TouchDevice::putRawEvent(const struct timeval&tv,int type,int code,int value
|
|||||||
mEvent.setActionButton(MotionEvent::BUTTON_PRIMARY);
|
mEvent.setActionButton(MotionEvent::BUTTON_PRIMARY);
|
||||||
mEvent.setAction(value?MotionEvent::ACTION_DOWN:MotionEvent::ACTION_UP);
|
mEvent.setAction(value?MotionEvent::ACTION_DOWN:MotionEvent::ACTION_UP);
|
||||||
if(value){
|
if(value){
|
||||||
mMoveTime = mDownTime =tv.tv_sec*1000+tv.tv_usec/1000;
|
mMoveTime = mDownTime =tv.tv_sec*1000000+tv.tv_usec;
|
||||||
mEvent.setButtonState(MotionEvent::BUTTON_PRIMARY);
|
mEvent.setButtonState(MotionEvent::BUTTON_PRIMARY);
|
||||||
}else{
|
}else{
|
||||||
mMoveTime =tv.tv_sec*1000+tv.tv_usec/1000;
|
mMoveTime =tv.tv_sec*1000000+tv.tv_usec;;
|
||||||
mEvent.setButtonState(mEvent.getButtonState()&(~MotionEvent::BUTTON_PRIMARY));
|
mEvent.setButtonState(mEvent.getButtonState()&(~MotionEvent::BUTTON_PRIMARY));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -329,7 +329,7 @@ int TouchDevice::putRawEvent(const struct timeval&tv,int type,int code,int value
|
|||||||
case EV_ABS:
|
case EV_ABS:
|
||||||
switch(code){
|
switch(code){
|
||||||
case ABS_X ... ABS_Z :
|
case ABS_X ... ABS_Z :
|
||||||
mMoveTime =tv.tv_sec*1000+tv.tv_usec/1000;
|
mMoveTime =tv.tv_sec*1000000+tv.tv_usec;
|
||||||
setAxisValue(0,code,value,false) ; break;
|
setAxisValue(0,code,value,false) ; break;
|
||||||
//case ABS_PRESSURE : setAxisValue(0,code,value,false) ; break;
|
//case ABS_PRESSURE : setAxisValue(0,code,value,false) ; break;
|
||||||
case ABS_MT_SLOT : mPointSlot=value ; break;
|
case ABS_MT_SLOT : mPointSlot=value ; break;
|
||||||
@ -354,6 +354,7 @@ int TouchDevice::putRawEvent(const struct timeval&tv,int type,int code,int value
|
|||||||
switch(code){
|
switch(code){
|
||||||
case SYN_REPORT:
|
case SYN_REPORT:
|
||||||
case SYN_MT_REPORT:
|
case SYN_MT_REPORT:
|
||||||
|
mMoveTime =(tv.tv_sec*1000000+tv.tv_usec);
|
||||||
LOGV("%s pos=%.f,%.f",MotionEvent::actionToString(mEvent.getAction()).c_str(),
|
LOGV("%s pos=%.f,%.f",MotionEvent::actionToString(mEvent.getAction()).c_str(),
|
||||||
mPointMAP.begin()->second.coord.getX(),mPointMAP.begin()->second.coord.getY() );
|
mPointMAP.begin()->second.coord.getX(),mPointMAP.begin()->second.coord.getY() );
|
||||||
mEvent.initialize(getId(),getSource(),mEvent.getAction(),mEvent.getActionButton(),
|
mEvent.initialize(getId(),getSource(),mEvent.getAction(),mEvent.getActionButton(),
|
||||||
|
@ -12,7 +12,7 @@ namespace cdroid{
|
|||||||
// stopped. We need to detect this case so that we can accurately predict the
|
// stopped. We need to detect this case so that we can accurately predict the
|
||||||
// velocity after the pointer starts moving again.
|
// velocity after the pointer starts moving again.
|
||||||
|
|
||||||
static const nsecs_t ASSUME_POINTER_STOPPED_TIME = 40;/*Millisecond*/
|
static const nsecs_t ASSUME_POINTER_STOPPED_TIME = 40*1000;/*Millisecond*/
|
||||||
|
|
||||||
static float vectorDot(const float* a, const float* b, uint32_t m) {
|
static float vectorDot(const float* a, const float* b, uint32_t m) {
|
||||||
float r = 0;
|
float r = 0;
|
||||||
@ -411,14 +411,14 @@ void IntegratingVelocityTrackerStrategy::initState(State& state,
|
|||||||
|
|
||||||
void IntegratingVelocityTrackerStrategy::updateState(State& state,
|
void IntegratingVelocityTrackerStrategy::updateState(State& state,
|
||||||
nsecs_t eventTime, float xpos, float ypos) const {
|
nsecs_t eventTime, float xpos, float ypos) const {
|
||||||
const nsecs_t MIN_TIME_DELTA = 2 ;
|
const nsecs_t MIN_TIME_DELTA = 2*1000 ;
|
||||||
const float FILTER_TIME_CONSTANT = 0.010f; // 10 milliseconds
|
const float FILTER_TIME_CONSTANT = 0.010f; // 10 milliseconds
|
||||||
|
|
||||||
if (eventTime <= state.updateTime + MIN_TIME_DELTA) {
|
if (eventTime <= state.updateTime + MIN_TIME_DELTA) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float dt = (eventTime - state.updateTime) *0.001f;//0.000000001f;
|
float dt = (eventTime - state.updateTime) *0.000001f;//0.000000001f;
|
||||||
state.updateTime = eventTime;
|
state.updateTime = eventTime;
|
||||||
|
|
||||||
float xvel = (xpos - state.xpos) / dt;
|
float xvel = (xpos - state.xpos) / dt;
|
||||||
@ -507,7 +507,7 @@ static float kineticEnergyToVelocity(float work) {
|
|||||||
static float calculateImpulseVelocity(const nsecs_t* t, const float* x, size_t count) {
|
static float calculateImpulseVelocity(const nsecs_t* t, const float* x, size_t count) {
|
||||||
// The input should be in reversed time order (most recent sample at index i=0)
|
// The input should be in reversed time order (most recent sample at index i=0)
|
||||||
// t[i] is in nanoseconds, but due to FP arithmetic, convert to seconds inside this function
|
// t[i] is in nanoseconds, but due to FP arithmetic, convert to seconds inside this function
|
||||||
static constexpr float SECONDS_PER_NANO = 1E-3;//android use nanosecond used value 1E-9;
|
static constexpr float SECONDS_PER_MICRO = 1E-6;//android use nanosecond used value 1E-9;
|
||||||
|
|
||||||
if (count < 2) {
|
if (count < 2) {
|
||||||
return 0; // if 0 or 1 points, velocity is zero
|
return 0; // if 0 or 1 points, velocity is zero
|
||||||
@ -520,7 +520,7 @@ static float calculateImpulseVelocity(const nsecs_t* t, const float* x, size_t c
|
|||||||
LOGE("Events have identical time stamps t=%lld, setting velocity = 0", t[0]);
|
LOGE("Events have identical time stamps t=%lld, setting velocity = 0", t[0]);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return (x[1] - x[0]) / (SECONDS_PER_NANO * (t[1] - t[0]));
|
return (x[1] - x[0]) / (SECONDS_PER_MICRO * (t[1] - t[0]));
|
||||||
}
|
}
|
||||||
// Guaranteed to have at least 3 points here
|
// Guaranteed to have at least 3 points here
|
||||||
float work = 0;
|
float work = 0;
|
||||||
@ -530,7 +530,7 @@ static float calculateImpulseVelocity(const nsecs_t* t, const float* x, size_t c
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
float vprev = kineticEnergyToVelocity(work); // v[i-1]
|
float vprev = kineticEnergyToVelocity(work); // v[i-1]
|
||||||
float vcurr = (x[i] - x[i-1]) / (SECONDS_PER_NANO * (t[i] - t[i-1])); // v[i]
|
float vcurr = (x[i] - x[i-1]) / (SECONDS_PER_MICRO * (t[i] - t[i-1])); // v[i]
|
||||||
work += (vcurr - vprev) * fabsf(vcurr);
|
work += (vcurr - vprev) * fabsf(vcurr);
|
||||||
if (i == count - 1) {
|
if (i == count - 1) {
|
||||||
work *= 0.5; // initial condition, case 2) above
|
work *= 0.5; // initial condition, case 2) above
|
||||||
@ -544,16 +544,16 @@ bool ImpulseVelocityTrackerStrategy::getEstimator(uint32_t id,
|
|||||||
outEstimator->clear();
|
outEstimator->clear();
|
||||||
|
|
||||||
// Iterate over movement samples in reverse time order and collect samples.
|
// Iterate over movement samples in reverse time order and collect samples.
|
||||||
float x[HISTORY_SIZE];
|
float x[HISTORY_SIZE]={.0f};
|
||||||
float y[HISTORY_SIZE];
|
float y[HISTORY_SIZE]={.0f};
|
||||||
nsecs_t time[HISTORY_SIZE];
|
nsecs_t time[HISTORY_SIZE]={0};
|
||||||
size_t m = 0; // number of points that will be used for fitting
|
size_t m = 0; // number of points that will be used for fitting
|
||||||
size_t index = mIndex;
|
size_t index = mIndex;
|
||||||
const Movement& newestMovement = mMovements[mIndex];
|
const Movement& newestMovement = mMovements[mIndex];
|
||||||
do {
|
do {
|
||||||
const Movement& movement = mMovements[index];
|
const Movement& movement = mMovements[index];
|
||||||
if (!movement.idBits.hasBit(id)) {
|
if (!movement.idBits.hasBit(id)) {
|
||||||
break;
|
LOGD("hasbit(%d)failed",id);break;
|
||||||
}
|
}
|
||||||
|
|
||||||
nsecs_t age = newestMovement.eventTime - movement.eventTime;
|
nsecs_t age = newestMovement.eventTime - movement.eventTime;
|
||||||
@ -808,7 +808,7 @@ bool LeastSquaresVelocityTrackerStrategy::getEstimator(uint32_t id,
|
|||||||
x[m] = position.x;
|
x[m] = position.x;
|
||||||
y[m] = position.y;
|
y[m] = position.y;
|
||||||
w[m] = chooseWeight(index);
|
w[m] = chooseWeight(index);
|
||||||
time[m] = -age * 0.001f;//android nanosecond use 0.000000001f;
|
time[m] = -age * 0.000001f;//android nanosecond use 0.000000001f;
|
||||||
index = (index == 0 ? HISTORY_SIZE : index) - 1;
|
index = (index == 0 ? HISTORY_SIZE : index) - 1;
|
||||||
} while (++m < HISTORY_SIZE);
|
} while (++m < HISTORY_SIZE);
|
||||||
|
|
||||||
@ -873,7 +873,7 @@ float LeastSquaresVelocityTrackerStrategy::chooseWeight(uint32_t index) const {
|
|||||||
|
|
||||||
uint32_t nextIndex = (index + 1) % HISTORY_SIZE;
|
uint32_t nextIndex = (index + 1) % HISTORY_SIZE;
|
||||||
float deltaMillis = (mMovements[nextIndex].eventTime- mMovements[index].eventTime)
|
float deltaMillis = (mMovements[nextIndex].eventTime- mMovements[index].eventTime)
|
||||||
* 0.000001f;
|
*0.001f;/*cdroid use us android usens 0.000001f*/;
|
||||||
if (deltaMillis < 0 ) return 0.5f;
|
if (deltaMillis < 0 ) return 0.5f;
|
||||||
if (deltaMillis < 10) return 0.5f + deltaMillis * 0.05;
|
if (deltaMillis < 10) return 0.5f + deltaMillis * 0.05;
|
||||||
return 1.0f;
|
return 1.0f;
|
||||||
@ -885,7 +885,7 @@ float LeastSquaresVelocityTrackerStrategy::chooseWeight(uint32_t index) const {
|
|||||||
// age 10ms: 1.0
|
// age 10ms: 1.0
|
||||||
// age 50ms: 1.0
|
// age 50ms: 1.0
|
||||||
// age 60ms: 0.5
|
// age 60ms: 0.5
|
||||||
float ageMillis = (mMovements[mIndex].eventTime - mMovements[index].eventTime) * 0.000001f;
|
float ageMillis = (mMovements[mIndex].eventTime - mMovements[index].eventTime) * 0.001f;/*android us ns 0.000001f*/;
|
||||||
if (ageMillis < 0 ) return 0.5f;
|
if (ageMillis < 0 ) return 0.5f;
|
||||||
if (ageMillis < 10) return 0.5f + ageMillis * 0.05;
|
if (ageMillis < 10) return 0.5f + ageMillis * 0.05;
|
||||||
if (ageMillis < 50) return 1.0f;
|
if (ageMillis < 50) return 1.0f;
|
||||||
@ -897,7 +897,7 @@ float LeastSquaresVelocityTrackerStrategy::chooseWeight(uint32_t index) const {
|
|||||||
// Weight points based on their age, weighing older points less.
|
// Weight points based on their age, weighing older points less.
|
||||||
// age 0ms: 1.0 , age 50ms: 1.0 , age 100ms: 0.5
|
// age 0ms: 1.0 , age 50ms: 1.0 , age 100ms: 0.5
|
||||||
float ageMillis = (mMovements[mIndex].eventTime - mMovements[index].eventTime)
|
float ageMillis = (mMovements[mIndex].eventTime - mMovements[index].eventTime)
|
||||||
* 0.000001f;
|
*0.001f;/*android use ns 0.000001f*/;
|
||||||
if (ageMillis < 50) {
|
if (ageMillis < 50) {
|
||||||
return 1.0f;
|
return 1.0f;
|
||||||
}
|
}
|
||||||
@ -934,7 +934,6 @@ void VelocityTracker::clear() {
|
|||||||
|
|
||||||
void VelocityTracker::addMovement(const MotionEvent& event) {
|
void VelocityTracker::addMovement(const MotionEvent& event) {
|
||||||
mVelocityTracker->addMovement(event);
|
mVelocityTracker->addMovement(event);
|
||||||
LOGV("%f,%f",event.getX(),event.getY());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void VelocityTracker::computeCurrentVelocity(int32_t units){
|
void VelocityTracker::computeCurrentVelocity(int32_t units){
|
||||||
|
@ -139,7 +139,7 @@ private:
|
|||||||
// Sample horizon.
|
// Sample horizon.
|
||||||
// We don't use too much history by default since we want to react to quick
|
// We don't use too much history by default since we want to react to quick
|
||||||
// changes in direction.
|
// changes in direction.
|
||||||
static const nsecs_t HORIZON = 100; //Millisecond
|
static const nsecs_t HORIZON = 100000; //Millisecond
|
||||||
// Number of samples to keep.
|
// Number of samples to keep.
|
||||||
static const uint32_t HISTORY_SIZE = 20;
|
static const uint32_t HISTORY_SIZE = 20;
|
||||||
struct Movement {
|
struct Movement {
|
||||||
@ -174,7 +174,7 @@ private:
|
|||||||
// Sample horizon.
|
// Sample horizon.
|
||||||
// We don't use too much history by default since we want to react to quick
|
// We don't use too much history by default since we want to react to quick
|
||||||
// changes in direction.
|
// changes in direction.
|
||||||
static constexpr nsecs_t HORIZON = 100; //Millisecond
|
static constexpr nsecs_t HORIZON = 100000; //Millisecond
|
||||||
|
|
||||||
// Number of samples to keep.
|
// Number of samples to keep.
|
||||||
static constexpr size_t HISTORY_SIZE = 20;
|
static constexpr size_t HISTORY_SIZE = 20;
|
||||||
|
@ -316,9 +316,8 @@ bool NumberPicker::onTouchEvent(MotionEvent& event){
|
|||||||
removeBeginSoftInputCommand();
|
removeBeginSoftInputCommand();
|
||||||
removeChangeCurrentByOneFromLongPress();
|
removeChangeCurrentByOneFromLongPress();
|
||||||
pshCancel();//mPressedStateHelper.cancel();
|
pshCancel();//mPressedStateHelper.cancel();
|
||||||
//VelocityTracker* velocityTracker = mVelocityTracker;
|
|
||||||
mVelocityTracker->computeCurrentVelocity(1000, mMaximumFlingVelocity);
|
mVelocityTracker->computeCurrentVelocity(1000, mMaximumFlingVelocity);
|
||||||
int initialVelocity = (int) mVelocityTracker->getYVelocity();
|
const int initialVelocity = (int) mVelocityTracker->getYVelocity();
|
||||||
if (std::abs(initialVelocity) > mMinimumFlingVelocity) {
|
if (std::abs(initialVelocity) > mMinimumFlingVelocity) {
|
||||||
fling(initialVelocity);
|
fling(initialVelocity);
|
||||||
onScrollStateChange(OnScrollListener::SCROLL_STATE_FLING);
|
onScrollStateChange(OnScrollListener::SCROLL_STATE_FLING);
|
||||||
|
@ -168,20 +168,18 @@ INT InputGetEvents(INPUTEVENT*outevents,UINT max,DWORD timeout){
|
|||||||
return E_ERROR;
|
return E_ERROR;
|
||||||
}
|
}
|
||||||
for(int i=0;i<dev.nfd;i++){
|
for(int i=0;i<dev.nfd;i++){
|
||||||
struct timespec ts;
|
|
||||||
if(!FD_ISSET(dev.fds[i],&rfds))continue;
|
if(!FD_ISSET(dev.fds[i],&rfds))continue;
|
||||||
if(dev.fds[i]!=dev.pipe[0]){
|
if(dev.fds[i]!=dev.pipe[0]){
|
||||||
clock_gettime(CLOCK_MONOTONIC,&ts);
|
|
||||||
rc=read(dev.fds[i],events, sizeof(events)/sizeof(struct input_event));
|
rc=read(dev.fds[i],events, sizeof(events)/sizeof(struct input_event));
|
||||||
for(int j=0;j<rc/sizeof(struct input_event)&&(count<max);j++,e++,count++){
|
for(int j=0;j<rc/sizeof(struct input_event)&&(count<max);j++,e++,count++){
|
||||||
e->tv_sec =ts.tv_sec;//events[j].time.tv_sec;
|
e->tv_sec = events[j].time.tv_sec;
|
||||||
e->tv_usec=ts.tv_nsec/10000+j*100;//events[j].time.tv_usec;
|
e->tv_usec= events[j].time.tv_usec;
|
||||||
e->type = events[j].type;
|
e->type = events[j].type;
|
||||||
e->code = events[j].code;
|
e->code = events[j].code;
|
||||||
e->value= events[j].value;
|
e->value= events[j].value;
|
||||||
e->device=dev.fds[i];
|
e->device=dev.fds[i];
|
||||||
LOGV_IF(e->type<EV_SW,"fd:%d [%s]%02x,%02x,%02x time=%ld.%ld",dev.fds[i],
|
LOGV_IF(e->type<EV_SW,"fd:%d [%s]%02x,%02x,%02x time=%ld.%ld time2=%ld.%ld",dev.fds[i],
|
||||||
type2name[e->type],e->type,e->code,e->value,e->tv_sec,e->tv_usec);
|
type2name[e->type],e->type,e->code,e->value,e->tv_sec,e->tv_usec,events[j].time.tv_sec,events[j].time.tv_usec);
|
||||||
}
|
}
|
||||||
}else{//for pipe
|
}else{//for pipe
|
||||||
rc=read(dev.fds[i],e, (max-count)*sizeof(INPUTEVENT));
|
rc=read(dev.fds[i],e, (max-count)*sizeof(INPUTEVENT));
|
||||||
|
@ -168,10 +168,8 @@ INT InputGetEvents(INPUTEVENT*outevents,UINT max,DWORD timeout){
|
|||||||
return E_ERROR;
|
return E_ERROR;
|
||||||
}
|
}
|
||||||
for(int i=0;i<dev.nfd;i++){
|
for(int i=0;i<dev.nfd;i++){
|
||||||
struct timespec ts;
|
|
||||||
if(!FD_ISSET(dev.fds[i],&rfds))continue;
|
if(!FD_ISSET(dev.fds[i],&rfds))continue;
|
||||||
if(dev.fds[i]!=dev.pipe[0]){
|
if(dev.fds[i]!=dev.pipe[0]){
|
||||||
clock_gettime(CLOCK_MONOTONIC,&ts);
|
|
||||||
rc=read(dev.fds[i],events, sizeof(events)/sizeof(struct input_event));
|
rc=read(dev.fds[i],events, sizeof(events)/sizeof(struct input_event));
|
||||||
for(int j=0;j<rc/sizeof(struct input_event)&&(count<max);j++,e++,count++){
|
for(int j=0;j<rc/sizeof(struct input_event)&&(count<max);j++,e++,count++){
|
||||||
e->tv_sec =events[j].time.tv_sec;
|
e->tv_sec =events[j].time.tv_sec;
|
||||||
|
Loading…
Reference in New Issue
Block a user