summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoDisplay.java
blob: 1fc34cb8bba5798739ac44165ac11df1d6b910ba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * vim: ts=4 sw=4 expandtab:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.geckoview;

import android.graphics.Bitmap;
import android.graphics.Rect;
import android.view.Surface;
import android.view.SurfaceControl;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import org.mozilla.gecko.util.ThreadUtils;

/**
 * Applications use a GeckoDisplay instance to provide {@link GeckoSession} with a {@link Surface}
 * for displaying content. To ensure drawing only happens on a valid {@link Surface}, {@link
 * GeckoSession} will only use the provided {@link Surface} after {@link
 * #surfaceChanged(SurfaceInfo)} is called and before {@link #surfaceDestroyed()} returns.
 */
public class GeckoDisplay {
  private final GeckoSession mSession;

  protected GeckoDisplay(final GeckoSession session) {
    mSession = session;
  }

  /**
   * Interface that allows Gecko the request a new Surface from the application. An implementation
   * of this should be set on the {@link GeckoDisplay.SurfaceInfo} object passed to {@link
   * GeckoDisplay#surfaceChanged(SurfaceInfo)}, by using {@link
   * GeckoDisplay.SurfaceInfo.Builder#newSurfaceProvider(NewSurfaceProvider)}.
   */
  public interface NewSurfaceProvider {
    /**
     * Called by Gecko to request a new Surface from the application.
     *
     * <p>Occasionally the Surface provided to Gecko via {@link #surfaceChanged(SurfaceInfo)} is
     * invalid and Gecko is unable to render in to it. This function will be called in such
     * circumstances. It is the implementation's responsibility to ensure that {@link
     * #surfaceChanged(SurfaceInfo)} gets called soon afterwards with a new Surface, allowing Gecko
     * to resume rendering.
     *
     * <p>Failure to implement this function may result in Gecko either crashing or not rendering
     * correctly should it encounter an invalid Surface.
     */
    @UiThread
    void requestNewSurface();
  }

  /**
   * Wrapper class containing a Surface and associated information that the compositor should render
   * in to. Should be constructed using {@link SurfaceInfo.Builder}.
   */
  public static class SurfaceInfo {
    /* package */ final @NonNull Surface mSurface;
    /* package */ final @Nullable SurfaceControl mSurfaceControl;
    /* package */ final @Nullable NewSurfaceProvider mNewSurfaceProvider;
    /* package */ final int mLeft;
    /* package */ final int mTop;
    /* package */ final int mWidth;
    /* package */ final int mHeight;

    private SurfaceInfo(final @NonNull Builder builder) {
      mSurface = builder.mSurface;
      mSurfaceControl = builder.mSurfaceControl;
      mNewSurfaceProvider = builder.mNewSurfaceProvider;
      mLeft = builder.mLeft;
      mTop = builder.mTop;
      mWidth = builder.mWidth;
      mHeight = builder.mHeight;
    }

    /** Helper class for constructing a {@link SurfaceInfo} object. */
    public static class Builder {
      private Surface mSurface;
      private SurfaceControl mSurfaceControl;
      private NewSurfaceProvider mNewSurfaceProvider;
      private int mLeft;
      private int mTop;
      private int mWidth;
      private int mHeight;

      /**
       * Creates a new Builder and sets the new Surface.
       *
       * @param surface The new Surface.
       */
      public Builder(final @NonNull Surface surface) {
        mSurface = surface;
      }

      /**
       * Sets the SurfaceControl associated with the new Surface's SurfaceView.
       *
       * <p>This must be called when rendering in to a {@link android.view.SurfaceView} on SDK level
       * 29 or above. On earlier SDK levels, or when rendering in to something other than a
       * SurfaceView, this call can be omitted or the value can be null.
       *
       * @param surfaceControl The SurfaceControl associated with the new Surface's SurfaceView, or
       *     null.
       * @return The builder object
       */
      @UiThread
      public @NonNull Builder surfaceControl(final @Nullable SurfaceControl surfaceControl) {
        mSurfaceControl = surfaceControl;
        return this;
      }

      /**
       * Sets a NewSurfaceProvider from which Gecko can request a new Surface.
       *
       * <p>This allows Gecko to recover from situations where the current Surface is for whatever
       * reason invalid and Gecko is unable to render in to it. Failure to set this field correctly
       * may result in Gecko either crashing or not rendering correctly should it encounter an
       * invalid Surface.
       *
       * @param newSurfaceProvider A NewSurfaceProvider from which Gecko can request a new Surface.
       * @return The builder object
       */
      @UiThread
      public @NonNull Builder newSurfaceProvider(
          final @Nullable NewSurfaceProvider newSurfaceProvider) {
        mNewSurfaceProvider = newSurfaceProvider;
        return this;
      }

      /**
       * Sets the new compositor origin offset.
       *
       * @param left The compositor origin offset in the X axis. Can not be negative.
       * @param top The compositor origin offset in the Y axis. Can not be negative.
       * @return The builder object
       */
      @UiThread
      public @NonNull Builder offset(final int left, final int top) {
        mLeft = left;
        mTop = top;
        return this;
      }

      /**
       * Sets the new surface size.
       *
       * @param width New width of the Surface. Can not be negative.
       * @param height New height of the Surface. Can not be negative.
       * @return The builder object
       */
      @UiThread
      public @NonNull Builder size(final int width, final int height) {
        mWidth = width;
        mHeight = height;
        return this;
      }

      /**
       * Builds the {@link SurfaceInfo} object with the specified properties.
       *
       * @return The SurfaceInfo object
       */
      @UiThread
      public @NonNull SurfaceInfo build() {
        if ((mLeft < 0) || (mTop < 0)) {
          throw new IllegalArgumentException("Left and Top offsets can not be negative.");
        }

        return new SurfaceInfo(this);
      }
    }
  }

  /**
   * Sets a surface for the compositor render a surface.
   *
   * <p>Required call. The display's Surface has been created or changed. Must be called on the
   * application main thread. GeckoSession may block this call to ensure the Surface is valid while
   * resuming drawing.
   *
   * <p>If rendering in to a {@link android.view.SurfaceView} on SDK level 29 or above, please
   * ensure that the SurfaceControl field of the {@link SurfaceInfo} object is set.
   *
   * @param surfaceInfo Information about the new Surface.
   */
  @UiThread
  public void surfaceChanged(@NonNull final SurfaceInfo surfaceInfo) {
    ThreadUtils.assertOnUiThread();

    if (mSession.getDisplay() == this) {
      mSession.onSurfaceChanged(surfaceInfo);
    }
  }

  /**
   * Removes the current surface registered with the compositor.
   *
   * <p>Required call. The display's Surface has been destroyed. Must be called on the application
   * main thread. GeckoSession may block this call to ensure the Surface is valid while pausing
   * drawing.
   */
  @UiThread
  public void surfaceDestroyed() {
    ThreadUtils.assertOnUiThread();

    if (mSession.getDisplay() == this) {
      mSession.onSurfaceDestroyed();
    }
  }

  /**
   * Update the position of the surface on the screen.
   *
   * <p>Optional call. The display's coordinates on the screen has changed. Must be called on the
   * application main thread.
   *
   * @param left The X coordinate of the display on the screen, in screen pixels.
   * @param top The Y coordinate of the display on the screen, in screen pixels.
   */
  @UiThread
  public void screenOriginChanged(final int left, final int top) {
    ThreadUtils.assertOnUiThread();

    if (mSession.getDisplay() == this) {
      mSession.onScreenOriginChanged(left, top);
    }
  }

  /**
   * Update the safe area insets of the surface on the screen.
   *
   * @param left left margin of safe area
   * @param top top margin of safe area
   * @param right right margin of safe area
   * @param bottom bottom margin of safe area
   */
  @UiThread
  public void safeAreaInsetsChanged(
      final int top, final int right, final int bottom, final int left) {
    ThreadUtils.assertOnUiThread();

    if (mSession.getDisplay() == this) {
      mSession.onSafeAreaInsetsChanged(top, right, bottom, left);
    }
  }

  /**
   * Set the maximum height of the dynamic toolbar(s).
   *
   * <p>If the toolbar is dynamic, this function needs to be called with the maximum possible
   * toolbar height so that Gecko can make the ICB static even during the dynamic toolbar height is
   * being changed.
   *
   * @param height The maximum height of the dynamic toolbar(s).
   */
  @UiThread
  public void setDynamicToolbarMaxHeight(final int height) {
    ThreadUtils.assertOnUiThread();

    if (mSession != null) {
      mSession.setDynamicToolbarMaxHeight(height);
    }
  }

  /**
   * Update the amount of vertical space that is clipped or visibly obscured in the bottom portion
   * of the display. Tells gecko where to put bottom fixed elements so they are fully visible.
   *
   * <p>Optional call. The display's visible vertical space has changed. Must be called on the
   * application main thread.
   *
   * @param clippingHeight The height of the bottom clipped space in screen pixels.
   */
  @UiThread
  public void setVerticalClipping(final int clippingHeight) {
    ThreadUtils.assertOnUiThread();

    if (mSession != null) {
      mSession.setFixedBottomOffset(clippingHeight);
    }
  }

  /**
   * Return whether the display should be pinned on the screen.
   *
   * <p>When pinned, the display should not be moved on the screen due to animation, scrolling, etc.
   * A common reason for the display being pinned is when the user is dragging a selection caret
   * inside the display; normal user interaction would be disrupted in that case if the display was
   * moved on screen.
   *
   * @return True if display should be pinned on the screen.
   */
  @UiThread
  public boolean shouldPinOnScreen() {
    ThreadUtils.assertOnUiThread();
    return mSession.getDisplay() == this && mSession.shouldPinOnScreen();
  }

  /**
   * Request a {@link Bitmap} of the visible portion of the web page currently being rendered.
   *
   * <p>Returned {@link Bitmap} will have the same dimensions as the {@link Surface} the {@link
   * GeckoDisplay} is currently using.
   *
   * <p>If the {@link GeckoSession#isCompositorReady} is false the {@link GeckoResult} will complete
   * with an {@link IllegalStateException}.
   *
   * <p>This function must be called on the UI thread.
   *
   * @return A {@link GeckoResult} that completes with a {@link Bitmap} containing the pixels and
   *     size information of the currently visible rendered web page.
   */
  @UiThread
  public @NonNull GeckoResult<Bitmap> capturePixels() {
    return screenshot().capture();
  }

  /** Builder to construct screenshot requests. */
  public static final class ScreenshotBuilder {
    private static final int NONE = 0;
    private static final int SCALE = 1;
    private static final int ASPECT = 2;
    private static final int FULL = 3;
    private static final int RECYCLE = 4;

    private final GeckoSession mSession;
    private int mOffsetX;
    private int mOffsetY;
    private int mSrcWidth;
    private int mSrcHeight;
    private int mOutWidth;
    private int mOutHeight;
    private int mAspectPreservingWidth;
    private float mScale;
    private Bitmap mRecycle;
    private int mSizeType;

    /* package */ ScreenshotBuilder(final GeckoSession session) {
      this.mSizeType = NONE;
      this.mSession = session;
    }

    /**
     * The screenshot will be of a region instead of the entire screen
     *
     * @param x Left most pixel of the source region.
     * @param y Top most pixel of the source region.
     * @param width Width of the source region in screen pixels
     * @param height Height of the source region in screen pixels
     * @return The builder
     */
    @AnyThread
    public @NonNull ScreenshotBuilder source(
        final int x, final int y, final int width, final int height) {
      mOffsetX = x;
      mOffsetY = y;
      mSrcWidth = width;
      mSrcHeight = height;
      return this;
    }

    /**
     * The screenshot will be of a region instead of the entire screen
     *
     * @param source Region of the screen to capture in screen pixels
     * @return The builder
     */
    @AnyThread
    public @NonNull ScreenshotBuilder source(final @NonNull Rect source) {
      mOffsetX = source.left;
      mOffsetY = source.top;
      mSrcWidth = source.width();
      mSrcHeight = source.height();
      return this;
    }

    private void checkAndSetSizeType(final int sizeType) {
      if (mSizeType != NONE) {
        throw new IllegalStateException("Size has already been set.");
      }
      mSizeType = sizeType;
    }

    /**
     * The width of the bitmap to create when taking the screenshot. The height will be calculated
     * to match the aspect ratio of the source as closely as possible. The source screenshot will be
     * scaled into the resulting Bitmap.
     *
     * @param width of the result Bitmap in screen pixels.
     * @return The builder
     * @throws IllegalStateException if the size has already been set in some other way.
     */
    @AnyThread
    public @NonNull ScreenshotBuilder aspectPreservingSize(final int width) {
      checkAndSetSizeType(ASPECT);
      mAspectPreservingWidth = width;
      return this;
    }

    /**
     * The scale of the bitmap relative to the source. The height and width of the output bitmap
     * will be within one pixel of this multiple of the source dimensions. The source screenshot
     * will be scaled into the resulting Bitmap.
     *
     * @param scale of the result Bitmap relative to the source.
     * @return The builder
     * @throws IllegalStateException if the size has already been set in some other way.
     */
    @AnyThread
    public @NonNull ScreenshotBuilder scale(final float scale) {
      checkAndSetSizeType(SCALE);
      mScale = scale;
      return this;
    }

    /**
     * Size of the bitmap to create when taking the screenshot. The source screenshot will be scaled
     * into the resulting Bitmap
     *
     * @param width of the result Bitmap in screen pixels.
     * @param height of the result Bitmap in screen pixels.
     * @return The builder
     * @throws IllegalStateException if the size has already been set in some other way.
     */
    @AnyThread
    public @NonNull ScreenshotBuilder size(final int width, final int height) {
      checkAndSetSizeType(FULL);
      mOutWidth = width;
      mOutHeight = height;
      return this;
    }

    /**
     * Instead of creating a new Bitmap for the result, the builder will use the passed Bitmap.
     *
     * @param bitmap The Bitmap to use in the result.
     * @return The builder.
     * @throws IllegalStateException if the size has already been set in some other way.
     */
    @AnyThread
    public @NonNull ScreenshotBuilder bitmap(final @Nullable Bitmap bitmap) {
      checkAndSetSizeType(RECYCLE);
      mRecycle = bitmap;
      return this;
    }

    /**
     * Request a {@link Bitmap} of the requested portion of the web page currently being rendered
     * using any parameters specified with the builder.
     *
     * <p>This function must be called on the UI thread.
     *
     * @return A {@link GeckoResult} that completes with a {@link Bitmap} containing the pixels and
     *     size information of the requested portion of the visible web page.
     */
    @UiThread
    public @NonNull GeckoResult<Bitmap> capture() {
      ThreadUtils.assertOnUiThread();
      if (!mSession.isCompositorReady()) {
        throw new IllegalStateException("Compositor must be ready before pixels can be captured");
      }

      final GeckoResult<Bitmap> result = new GeckoResult<>();
      final Bitmap target;
      final Rect rect = new Rect();

      if (mSrcWidth == 0 || mSrcHeight == 0) {
        // Source is unset or invalid, use defaults.
        mSession.getSurfaceBounds(rect);
        mSrcWidth = rect.width();
        mSrcHeight = rect.height();
      }

      switch (mSizeType) {
        case NONE:
          mOutWidth = mSrcWidth;
          mOutHeight = mSrcHeight;
          break;
        case SCALE:
          mSession.getSurfaceBounds(rect);
          mOutWidth = (int) (rect.width() * mScale);
          mOutHeight = (int) (rect.height() * mScale);
          break;
        case ASPECT:
          mSession.getSurfaceBounds(rect);
          mOutWidth = mAspectPreservingWidth;
          mOutHeight = (int) (rect.height() * (mAspectPreservingWidth / (double) rect.width()));
          break;
        case RECYCLE:
          mOutWidth = mRecycle.getWidth();
          mOutHeight = mRecycle.getHeight();
          break;
          // case FULL does not need to be handled, as width and height are already set.
      }

      if (mRecycle == null) {
        try {
          target = Bitmap.createBitmap(mOutWidth, mOutHeight, Bitmap.Config.ARGB_8888);
        } catch (final Throwable e) {
          if (e instanceof NullPointerException || e instanceof OutOfMemoryError) {
            return GeckoResult.fromException(
                new OutOfMemoryError("Not enough memory to allocate for bitmap"));
          }
          return GeckoResult.fromException(new Throwable("Failed to create bitmap", e));
        }
      } else {
        target = mRecycle;
      }

      mSession.mCompositor.requestScreenPixels(
          result, target, mOffsetX, mOffsetY, mSrcWidth, mSrcHeight, mOutWidth, mOutHeight);

      return result;
    }
  }

  /**
   * Creates a new screenshot builder.
   *
   * @return The new {@link ScreenshotBuilder}
   */
  @UiThread
  public @NonNull ScreenshotBuilder screenshot() {
    return new ScreenshotBuilder(mSession);
  }
}