| Line 125... |
Line 125... |
| 125 |
String fileName = "/tmp/oppo-simple-i-" + counter + " " + System.currentTimeMillis() + ".png";
|
125 |
String fileName = "/tmp/oppo-simple-i-" + counter + " " + System.currentTimeMillis() + ".png";
|
| 126 |
try {
|
126 |
try {
|
| 127 |
LOGGER.info("Starting Do");
|
127 |
LOGGER.info("Starting Do");
|
| 128 |
wait10Sec.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[starts-with(@id, 'dx_captcha_basic_wrapper_')]")));
|
128 |
wait10Sec.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[starts-with(@id, 'dx_captcha_basic_wrapper_')]")));
|
| 129 |
wait10Sec.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[starts-with(@id, 'dx_captcha_basic_slider_')]")));
|
129 |
wait10Sec.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//*[starts-with(@id, 'dx_captcha_basic_slider_')]")));
|
| 130 |
slideButton = driver.findElement(By.xpath("//*[starts-with(@id, 'dx_captcha_basic_slider_')]"));
|
130 |
By sliderBy = By.xpath("//*[starts-with(@id, 'dx_captcha_basic_slider_')]");
|
| 131 |
//System.out.println("Slide button - " + slideButton);
|
131 |
slideButton = driver.findElement(sliderBy);
|
| 132 |
LOGGER.info("Margin Left before- " + slideButton.getCssValue("margin-left"));
|
132 |
LOGGER.info("Margin Left before- " + marginLeftOf(slideButton));
|
| - |
|
133 |
try {
|
| - |
|
134 |
actionProvider.moveToElement(slideButton).clickAndHold()
|
| - |
|
135 |
.moveByOffset(1, 0)
|
| - |
|
136 |
.perform();
|
| - |
|
137 |
} catch (StaleElementReferenceException stale) {
|
| - |
|
138 |
// The retry refresh (doubleClick at the end of the previous
|
| - |
|
139 |
// attempt) swaps the widget's DOM, so the handle located at the
|
| - |
|
140 |
// top of THIS attempt can already be detached by the time we act
|
| - |
|
141 |
// on it. Re-find and try once, rather than abandoning the imei
|
| - |
|
142 |
// and reloading the page: this killed 19% of oppo attempts
|
| - |
|
143 |
// (54 of 284), against 0 on realme.
|
| - |
|
144 |
LOGGER.info("Slider went stale before the drag - re-finding and retrying once");
|
| - |
|
145 |
slideButton = driver.findElement(sliderBy);
|
| 133 |
actionProvider.moveToElement(slideButton).clickAndHold()
|
146 |
actionProvider.moveToElement(slideButton).clickAndHold()
|
| 134 |
.moveByOffset(1, 0)//.pause(2000)
|
147 |
.moveByOffset(1, 0)
|
| 135 |
.perform();
|
148 |
.perform();
|
| - |
|
149 |
}
|
| 136 |
pressed = true;
|
150 |
pressed = true;
|
| 137 |
|
- |
|
| 138 |
//actionProvider.moveByOffset(-1,0).pause(2000).perform();
|
- |
|
| 139 |
LOGGER.info("Margin Left after - " + slideButton.getCssValue("margin-left"));
|
151 |
LOGGER.info("Margin Left after - " + marginLeftOf(slideButton));
|
| 140 |
//Thread.sleep(5000);
|
152 |
//Thread.sleep(5000);
|
| 141 |
} catch (Exception e) {
|
153 |
} catch (Exception e) {
|
| 142 |
LOGGER.warn("Slider captcha widget (dx_captcha_basic_*) not found or not draggable"
|
154 |
LOGGER.warn("Slider captcha widget (dx_captcha_basic_*) not found or not draggable"
|
| 143 |
+ " - reloading. Cause: " + e.getMessage());
|
155 |
+ " - reloading. Cause: " + e.getMessage());
|
| 144 |
driver.get("https://support.oppo.com/in/warranty-check/");
|
156 |
driver.get("https://support.oppo.com/in/warranty-check/");
|
| Line 150... |
Line 162... |
| 150 |
double firstCircleX = circles[0];
|
162 |
double firstCircleX = circles[0];
|
| 151 |
double secondCircleX = circles[1];
|
163 |
double secondCircleX = circles[1];
|
| 152 |
double distance = Math.abs(firstCircleX - secondCircleX);
|
164 |
double distance = Math.abs(firstCircleX - secondCircleX);
|
| 153 |
LOGGER.info("Distance is " + distance);
|
165 |
LOGGER.info("Distance is " + distance);
|
| 154 |
|
166 |
|
| 155 |
double[] cal = calibrate(driver, actionProvider, distance);
|
167 |
// Closed loop: no assumed ratio, every move re-measures.
|
| 156 |
int moveByOffset = cal == null ? 0 : (int) cal[0];
|
168 |
int moveByOffset = aim(driver, actionProvider, distance);
|
| 157 |
if (moveByOffset > 0) {
|
169 |
if (moveByOffset > 0) {
|
| 158 |
// Move, then measure again and close the remainder BEFORE
|
- |
|
| 159 |
// releasing. Releasing on the first computed offset committed
|
- |
|
| 160 |
// the whole calibration error to the final position.
|
- |
|
| 161 |
actionProvider.moveByOffset(moveByOffset, 0).perform();
|
- |
|
| 162 |
refine(driver, actionProvider, cal[1]);
|
- |
|
| 163 |
actionProvider.release().perform();
|
170 |
actionProvider.release().perform();
|
| 164 |
pressed = false;
|
171 |
pressed = false;
|
| 165 |
LOGGER.info("Move click performed (main {}px)", moveByOffset);
|
172 |
LOGGER.info("Move click performed (main {}px)", moveByOffset);
|
| 166 |
try {
|
173 |
try {
|
| 167 |
try {
|
174 |
try {
|
| Line 395... |
Line 402... |
| 395 |
*
|
402 |
*
|
| 396 |
* Aim to close about half the gap: the piece travels roughly ASSUMED_RATIO px per
|
403 |
* Aim to close about half the gap: the piece travels roughly ASSUMED_RATIO px per
|
| 397 |
* slider px, so nudge ~= gap / (2 * ratio), clamped. The real ratio is still derived
|
404 |
* slider px, so nudge ~= gap / (2 * ratio), clamped. The real ratio is still derived
|
| 398 |
* from the measurement; this only sizes the probe.
|
405 |
* from the measurement; this only sizes the probe.
|
| 399 |
*/
|
406 |
*/
|
| 400 |
/*
|
- |
|
| 401 |
* These are OPPO's tuning values and are deliberately independent of realme's.
|
- |
|
| 402 |
* The two classes drive the same dingxiang widget but convert very differently
|
- |
|
| 403 |
* (oppo ~12%, realme ~60% of completed drags), so a change proven on one is NOT
|
- |
|
| 404 |
* evidence for the other. Tune and measure them separately.
|
- |
|
| 405 |
*
|
- |
|
| 406 |
* Back to 1.7 after r37433 set this to 0.67 and it did not help (14% -> 13%).
|
- |
|
| 407 |
*
|
- |
|
| 408 |
* 0.67 was fitted to 3,694 production calibrations -- but every one of those was
|
- |
|
| 409 |
* measured while the slider press was being leaked between attempts (r37435), so the
|
- |
|
| 410 |
* sample was contaminated and the value fitted to noise.
|
- |
|
| 411 |
*
|
- |
|
| 412 |
* With that confound removed the clean evidence is realme: identical widget, same
|
- |
|
| 413 |
* calibration code, ASSUMED_RATIO 1.7, converting 82%. Measured post-fix on similar
|
- |
|
| 414 |
* gaps, 1.7 probes ~31px and closes ~11% of the gap, while 0.67 probes ~52px and
|
- |
|
| 415 |
* closes ~37%. The SMALLER probe is the one that lands -- a probe that eats a third
|
- |
|
| 416 |
* of the gap risks crossing the target during calibration, and |c1-c2| cannot tell
|
- |
|
| 417 |
* "not there yet" from "gone past", so the derived ratio comes back wrong.
|
- |
|
| 418 |
*/
|
- |
|
| 419 |
private static final double ASSUMED_RATIO = 1.7;
|
- |
|
| 420 |
private static final int MIN_NUDGE = 10;
|
- |
|
| 421 |
private static final int MAX_NUDGE = 60;
|
- |
|
| 422 |
private static final double ALIGNED_PX = 2.0;
|
407 |
private static final double ALIGNED_PX = 2.0;
|
| 423 |
|
408 |
|
| 424 |
private static int nudgeFor(double gap) {
|
- |
|
| 425 |
int n = (int) Math.round(gap / (2 * ASSUMED_RATIO));
|
- |
|
| 426 |
return Math.max(MIN_NUDGE, Math.min(MAX_NUDGE, n));
|
- |
|
| 427 |
}
|
- |
|
| 428 |
|
- |
|
| 429 |
/**
|
409 |
/**
|
| 430 |
* @return {sliderPxToMove, sliderPxPerCirclePx}, or null when the circles could not
|
410 |
* margin-left for logging only. Reading it off a detached element throws, and this is
|
| 431 |
* be read. Returned rather than stored on a field: this is a singleton and
|
411 |
* a diagnostic -- it must never be the reason an attempt is abandoned.
|
| 432 |
* the Oppo secondary and tertiary jobs run it concurrently.
|
- |
|
| 433 |
*/
|
412 |
*/
|
| 434 |
private double[] calibrate(WebDriver driver, Actions actionProvider, double distance) throws Exception {
|
- |
|
| 435 |
int nudge = nudgeFor(distance);
|
- |
|
| 436 |
actionProvider.moveByOffset(nudge, 0).perform();
|
413 |
private String marginLeftOf(WebElement el) {
|
| 437 |
|
- |
|
| 438 |
String fileName2 = "/tmp/" + Thread.currentThread().getName() + "-moved.png";
|
- |
|
| 439 |
double[] movedCircles = getMatCircles2(driver, fileName2);
|
- |
|
| 440 |
if (movedCircles == null) return null;
|
414 |
try {
|
| 441 |
|
- |
|
| 442 |
double movedDistance = Math.abs(movedCircles[0] - movedCircles[1]);
|
- |
|
| 443 |
double closed = distance - movedDistance;
|
415 |
return el.getCssValue("margin-left");
|
| 444 |
LOGGER.info("Calibration: gap {} -> {} after {}px, closed {}",
|
- |
|
| 445 |
distance, movedDistance, nudge, closed);
|
416 |
} catch (Exception e) {
|
| 446 |
|
- |
|
| 447 |
// A non-positive value means the piece did not move, or moved away from the
|
- |
|
| 448 |
// hole -- either way the ratio is meaningless and guessing would waste the try.
|
- |
|
| 449 |
if (closed <= 0) return null;
|
417 |
return "?";
|
| 450 |
|
- |
|
| 451 |
double sliderPerCircle = nudge / closed;
|
418 |
}
|
| 452 |
int moveBy = (int) Math.round(movedDistance * sliderPerCircle);
|
- |
|
| 453 |
return new double[]{moveBy, sliderPerCircle};
|
- |
|
| 454 |
}
|
419 |
}
|
| 455 |
|
420 |
|
| - |
|
421 |
/** Seed probe, in slider px. Deliberately small: it only has to produce a first
|
| - |
|
422 |
* measurable movement, and it must be too small to cross the hole. */
|
| - |
|
423 |
private static final int SEED_NUDGE = 12;
|
| - |
|
424 |
private static final int MAX_AIM_STEPS = 5;
|
| - |
|
425 |
private static final int MAX_STEP = 140;
|
| - |
|
426 |
|
| 456 |
/**
|
427 |
/**
|
| - |
|
428 |
* Drag onto the hole without ever assuming a ratio.
|
| - |
|
429 |
*
|
| - |
|
430 |
* The old path derived px-per-px ONCE from a single probe and then trusted it for
|
| - |
|
431 |
* the main move and the correction alike, so one noisy probe poisoned everything
|
| - |
|
432 |
* downstream. Here every move is also a measurement: after each one the ratio is
|
| 457 |
* Measure again after the main move and close whatever is left. The old code
|
433 |
* re-derived from what that move actually achieved, so the estimate improves as the
|
| 458 |
* committed its single computed offset blind, so any calibration error went
|
434 |
* piece approaches instead of being fixed at its least informed point.
|
| - |
|
435 |
*
|
| - |
|
436 |
* It also gets to see overshoot, which a single pass cannot. The gap is |c1 - c2|,
|
| - |
|
437 |
* so it cannot tell "not there yet" from "gone past" -- but if a move makes the gap
|
| 459 |
* straight into the final position with nothing to catch it.
|
438 |
* GROW, we crossed the hole, and the sign of the next step flips.
|
| - |
|
439 |
*
|
| - |
|
440 |
* @return total slider px moved, or 0 if the circles could not be read at all.
|
| 460 |
*/
|
441 |
*/
|
| 461 |
private int refine(WebDriver driver, Actions actionProvider, double sliderPerCircle) throws Exception {
|
442 |
private int aim(WebDriver driver, Actions actionProvider, double gap) throws Exception {
|
| - |
|
443 |
double ratio = 0; // slider px per gap px, derived from real moves
|
| - |
|
444 |
int total = 0;
|
| - |
|
445 |
int step = SEED_NUDGE;
|
| - |
|
446 |
|
| - |
|
447 |
for (int i = 0; i < MAX_AIM_STEPS; i++) {
|
| - |
|
448 |
actionProvider.moveByOffset(step, 0).perform();
|
| - |
|
449 |
total += step;
|
| - |
|
450 |
|
| 462 |
String fileName = "/tmp/" + Thread.currentThread().getName() + "-refine.png";
|
451 |
double[] c = getMatCircles2(driver, "/tmp/" + Thread.currentThread().getName() + "-aim" + i + ".png");
|
| - |
|
452 |
if (c == null) {
|
| - |
|
453 |
LOGGER.warn("Aim step {}: lost the circles after {}px", i, total);
|
| - |
|
454 |
return total;
|
| - |
|
455 |
}
|
| 463 |
double[] circles = getMatCircles2(driver, fileName);
|
456 |
double now = Math.abs(c[0] - c[1]);
|
| 464 |
if (circles == null) return 0;
|
457 |
double closed = gap - now;
|
| 465 |
|
458 |
|
| - |
|
459 |
if (closed != 0) {
|
| - |
|
460 |
// What this move actually bought us, in slider px per gap px.
|
| 466 |
double remaining = Math.abs(circles[0] - circles[1]);
|
461 |
double observed = Math.abs(step) / Math.abs(closed);
|
| - |
|
462 |
// Blend once an estimate exists, so a single bad frame cannot swing it.
|
| - |
|
463 |
ratio = (ratio <= 0) ? observed : (ratio + observed) / 2;
|
| - |
|
464 |
}
|
| - |
|
465 |
LOGGER.info("Aim step {}: moved {}px, gap {} -> {}, ratio now {}",
|
| - |
|
466 |
i, step, gap, now, String.format("%.2f", ratio));
|
| - |
|
467 |
|
| 467 |
if (remaining <= ALIGNED_PX) return 0;
|
468 |
if (now <= ALIGNED_PX) {
|
| - |
|
469 |
LOGGER.info("Aim: aligned within {}px after {} step(s), {}px total", ALIGNED_PX, i + 1, total);
|
| - |
|
470 |
return total;
|
| - |
|
471 |
}
|
| - |
|
472 |
if (ratio <= 0) { // nothing measurable yet, probe again
|
| - |
|
473 |
gap = now;
|
| - |
|
474 |
step = SEED_NUDGE;
|
| - |
|
475 |
continue;
|
| - |
|
476 |
}
|
| 468 |
|
477 |
|
| 469 |
int correction = (int) Math.round(remaining * sliderPerCircle);
|
478 |
int next = (int) Math.round(now * ratio);
|
| 470 |
if (correction == 0) return 0;
|
479 |
if (next == 0) next = 1;
|
| - |
|
480 |
if (next > MAX_STEP) next = MAX_STEP;
|
| 471 |
LOGGER.info("Refine: {}px of gap left -> nudging {}px", remaining, correction);
|
481 |
// Gap grew => we are past the hole; go back instead of further away.
|
| 472 |
actionProvider.moveByOffset(correction, 0).perform();
|
482 |
step = (closed < 0) ? -next : next;
|
| - |
|
483 |
gap = now;
|
| - |
|
484 |
}
|
| - |
|
485 |
LOGGER.info("Aim: {} steps used, {}px total", MAX_AIM_STEPS, total);
|
| 473 |
return correction;
|
486 |
return total;
|
| 474 |
}
|
487 |
}
|
| 475 |
|
488 |
|
| - |
|
489 |
|
| - |
|
490 |
|
| 476 |
private Mat getMatCircles(WebDriver driver, String fileName) throws Exception {
|
491 |
private Mat getMatCircles(WebDriver driver, String fileName) throws Exception {
|
| 477 |
|
492 |
|
| 478 |
File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
|
493 |
File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
|
| 479 |
BufferedImage fullImg = ImageIO.read(screenshot);
|
494 |
BufferedImage fullImg = ImageIO.read(screenshot);
|
| 480 |
|
495 |
|