Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.view;
     18 
     19 import static android.util.DisplayMetrics.DENSITY_DEFAULT;
     20 import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
     21 import static android.view.DisplayCutoutProto.BOUNDS;
     22 import static android.view.DisplayCutoutProto.INSETS;
     23 
     24 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
     25 
     26 import android.content.res.Resources;
     27 import android.graphics.Matrix;
     28 import android.graphics.Path;
     29 import android.graphics.Rect;
     30 import android.graphics.RectF;
     31 import android.graphics.Region;
     32 import android.os.Parcel;
     33 import android.os.Parcelable;
     34 import android.text.TextUtils;
     35 import android.util.Log;
     36 import android.util.Pair;
     37 import android.util.PathParser;
     38 import android.util.proto.ProtoOutputStream;
     39 
     40 import com.android.internal.R;
     41 import com.android.internal.annotations.GuardedBy;
     42 import com.android.internal.annotations.VisibleForTesting;
     43 
     44 import java.util.ArrayList;
     45 import java.util.List;
     46 
     47 /**
     48  * Represents the area of the display that is not functional for displaying content.
     49  *
     50  * <p>{@code DisplayCutout} is immutable.
     51  */
     52 public final class DisplayCutout {
     53 
     54     private static final String TAG = "DisplayCutout";
     55     private static final String BOTTOM_MARKER = "@bottom";
     56     private static final String DP_MARKER = "@dp";
     57     private static final String RIGHT_MARKER = "@right";
     58 
     59     /**
     60      * Category for overlays that allow emulating a display cutout on devices that don't have
     61      * one.
     62      *
     63      * @see android.content.om.IOverlayManager
     64      * @hide
     65      */
     66     public static final String EMULATION_OVERLAY_CATEGORY =
     67             "com.android.internal.display_cutout_emulation";
     68 
     69     private static final Rect ZERO_RECT = new Rect();
     70     private static final Region EMPTY_REGION = new Region();
     71 
     72     /**
     73      * An instance where {@link #isEmpty()} returns {@code true}.
     74      *
     75      * @hide
     76      */
     77     public static final DisplayCutout NO_CUTOUT = new DisplayCutout(ZERO_RECT, EMPTY_REGION,
     78             false /* copyArguments */);
     79 
     80 
     81     private static final Pair<Path, DisplayCutout> NULL_PAIR = new Pair<>(null, null);
     82     private static final Object CACHE_LOCK = new Object();
     83 
     84     @GuardedBy("CACHE_LOCK")
     85     private static String sCachedSpec;
     86     @GuardedBy("CACHE_LOCK")
     87     private static int sCachedDisplayWidth;
     88     @GuardedBy("CACHE_LOCK")
     89     private static int sCachedDisplayHeight;
     90     @GuardedBy("CACHE_LOCK")
     91     private static float sCachedDensity;
     92     @GuardedBy("CACHE_LOCK")
     93     private static Pair<Path, DisplayCutout> sCachedCutout = NULL_PAIR;
     94 
     95     private final Rect mSafeInsets;
     96     private final Region mBounds;
     97 
     98     /**
     99      * Creates a DisplayCutout instance.
    100      *
    101      * @param safeInsets the insets from each edge which avoid the display cutout as returned by
    102      *                   {@link #getSafeInsetTop()} etc.
    103      * @param boundingRects the bounding rects of the display cutouts as returned by
    104      *               {@link #getBoundingRects()} ()}.
    105      */
    106     // TODO(b/73953958): @VisibleForTesting(visibility = PRIVATE)
    107     public DisplayCutout(Rect safeInsets, List<Rect> boundingRects) {
    108         this(safeInsets != null ? new Rect(safeInsets) : ZERO_RECT,
    109                 boundingRectsToRegion(boundingRects),
    110                 true /* copyArguments */);
    111     }
    112 
    113     /**
    114      * Creates a DisplayCutout instance.
    115      *
    116      * @param copyArguments if true, create a copy of the arguments. If false, the passed arguments
    117      *                      are not copied and MUST remain unchanged forever.
    118      */
    119     private DisplayCutout(Rect safeInsets, Region bounds, boolean copyArguments) {
    120         mSafeInsets = safeInsets == null ? ZERO_RECT :
    121                 (copyArguments ? new Rect(safeInsets) : safeInsets);
    122         mBounds = bounds == null ? Region.obtain() :
    123                 (copyArguments ? Region.obtain(bounds) : bounds);
    124     }
    125 
    126     /**
    127      * Returns true if the safe insets are empty (and therefore the current view does not
    128      * overlap with the cutout or cutout area).
    129      *
    130      * @hide
    131      */
    132     public boolean isEmpty() {
    133         return mSafeInsets.equals(ZERO_RECT);
    134     }
    135 
    136     /**
    137      * Returns true if there is no cutout, i.e. the bounds are empty.
    138      *
    139      * @hide
    140      */
    141     public boolean isBoundsEmpty() {
    142         return mBounds.isEmpty();
    143     }
    144 
    145     /** Returns the inset from the top which avoids the display cutout in pixels. */
    146     public int getSafeInsetTop() {
    147         return mSafeInsets.top;
    148     }
    149 
    150     /** Returns the inset from the bottom which avoids the display cutout in pixels. */
    151     public int getSafeInsetBottom() {
    152         return mSafeInsets.bottom;
    153     }
    154 
    155     /** Returns the inset from the left which avoids the display cutout in pixels. */
    156     public int getSafeInsetLeft() {
    157         return mSafeInsets.left;
    158     }
    159 
    160     /** Returns the inset from the right which avoids the display cutout in pixels. */
    161     public int getSafeInsetRight() {
    162         return mSafeInsets.right;
    163     }
    164 
    165     /**
    166      * Returns the safe insets in a rect in pixel units.
    167      *
    168      * @return a rect which is set to the safe insets.
    169      * @hide
    170      */
    171     public Rect getSafeInsets() {
    172         return new Rect(mSafeInsets);
    173     }
    174 
    175     /**
    176      * Returns the bounding region of the cutout.
    177      *
    178      * <p>
    179      * <strong>Note:</strong> There may be more than one cutout, in which case the returned
    180      * {@code Region} will be non-contiguous and its bounding rect will be meaningless without
    181      * intersecting it first.
    182      *
    183      * Example:
    184      * <pre>
    185      *     // Getting the bounding rectangle of the top display cutout
    186      *     Region bounds = displayCutout.getBounds();
    187      *     bounds.op(0, 0, Integer.MAX_VALUE, displayCutout.getSafeInsetTop(), Region.Op.INTERSECT);
    188      *     Rect topDisplayCutout = bounds.getBoundingRect();
    189      * </pre>
    190      *
    191      * @return the bounding region of the cutout. Coordinates are relative
    192      *         to the top-left corner of the content view and in pixel units.
    193      * @hide
    194      */
    195     public Region getBounds() {
    196         return Region.obtain(mBounds);
    197     }
    198 
    199     /**
    200      * Returns a list of {@code Rect}s, each of which is the bounding rectangle for a non-functional
    201      * area on the display.
    202      *
    203      * There will be at most one non-functional area per short edge of the device, and none on
    204      * the long edges.
    205      *
    206      * @return a list of bounding {@code Rect}s, one for each display cutout area.
    207      */
    208     public List<Rect> getBoundingRects() {
    209         List<Rect> result = new ArrayList<>();
    210         Region bounds = Region.obtain();
    211         // top
    212         bounds.set(mBounds);
    213         bounds.op(0, 0, Integer.MAX_VALUE, getSafeInsetTop(), Region.Op.INTERSECT);
    214         if (!bounds.isEmpty()) {
    215             result.add(bounds.getBounds());
    216         }
    217         // left
    218         bounds.set(mBounds);
    219         bounds.op(0, 0, getSafeInsetLeft(), Integer.MAX_VALUE, Region.Op.INTERSECT);
    220         if (!bounds.isEmpty()) {
    221             result.add(bounds.getBounds());
    222         }
    223         // right & bottom
    224         bounds.set(mBounds);
    225         bounds.op(getSafeInsetLeft() + 1, getSafeInsetTop() + 1,
    226                 Integer.MAX_VALUE, Integer.MAX_VALUE, Region.Op.INTERSECT);
    227         if (!bounds.isEmpty()) {
    228             result.add(bounds.getBounds());
    229         }
    230         bounds.recycle();
    231         return result;
    232     }
    233 
    234     @Override
    235     public int hashCode() {
    236         int result = mSafeInsets.hashCode();
    237         result = result * 31 + mBounds.getBounds().hashCode();
    238         return result;
    239     }
    240 
    241     @Override
    242     public boolean equals(Object o) {
    243         if (o == this) {
    244             return true;
    245         }
    246         if (o instanceof DisplayCutout) {
    247             DisplayCutout c = (DisplayCutout) o;
    248             return mSafeInsets.equals(c.mSafeInsets)
    249                     && mBounds.equals(c.mBounds);
    250         }
    251         return false;
    252     }
    253 
    254     @Override
    255     public String toString() {
    256         return "DisplayCutout{insets=" + mSafeInsets
    257                 + " boundingRect=" + mBounds.getBounds()
    258                 + "}";
    259     }
    260 
    261     /**
    262      * @hide
    263      */
    264     public void writeToProto(ProtoOutputStream proto, long fieldId) {
    265         final long token = proto.start(fieldId);
    266         mSafeInsets.writeToProto(proto, INSETS);
    267         mBounds.getBounds().writeToProto(proto, BOUNDS);
    268         proto.end(token);
    269     }
    270 
    271     /**
    272      * Insets the reference frame of the cutout in the given directions.
    273      *
    274      * @return a copy of this instance which has been inset
    275      * @hide
    276      */
    277     public DisplayCutout inset(int insetLeft, int insetTop, int insetRight, int insetBottom) {
    278         if (mBounds.isEmpty()
    279                 || insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0) {
    280             return this;
    281         }
    282 
    283         Rect safeInsets = new Rect(mSafeInsets);
    284         Region bounds = Region.obtain(mBounds);
    285 
    286         // Note: it's not really well defined what happens when the inset is negative, because we
    287         // don't know if the safe inset needs to expand in general.
    288         if (insetTop > 0 || safeInsets.top > 0) {
    289             safeInsets.top = atLeastZero(safeInsets.top - insetTop);
    290         }
    291         if (insetBottom > 0 || safeInsets.bottom > 0) {
    292             safeInsets.bottom = atLeastZero(safeInsets.bottom - insetBottom);
    293         }
    294         if (insetLeft > 0 || safeInsets.left > 0) {
    295             safeInsets.left = atLeastZero(safeInsets.left - insetLeft);
    296         }
    297         if (insetRight > 0 || safeInsets.right > 0) {
    298             safeInsets.right = atLeastZero(safeInsets.right - insetRight);
    299         }
    300 
    301         bounds.translate(-insetLeft, -insetTop);
    302         return new DisplayCutout(safeInsets, bounds, false /* copyArguments */);
    303     }
    304 
    305     /**
    306      * Returns a copy of this instance with the safe insets replaced with the parameter.
    307      *
    308      * @param safeInsets the new safe insets in pixels
    309      * @return a copy of this instance with the safe insets replaced with the argument.
    310      *
    311      * @hide
    312      */
    313     public DisplayCutout replaceSafeInsets(Rect safeInsets) {
    314         return new DisplayCutout(new Rect(safeInsets), mBounds, false /* copyArguments */);
    315     }
    316 
    317     private static int atLeastZero(int value) {
    318         return value < 0 ? 0 : value;
    319     }
    320 
    321 
    322     /**
    323      * Creates an instance from a bounding rect.
    324      *
    325      * @hide
    326      */
    327     public static DisplayCutout fromBoundingRect(int left, int top, int right, int bottom) {
    328         Path path = new Path();
    329         path.reset();
    330         path.moveTo(left, top);
    331         path.lineTo(left, bottom);
    332         path.lineTo(right, bottom);
    333         path.lineTo(right, top);
    334         path.close();
    335         return fromBounds(path);
    336     }
    337 
    338     /**
    339      * Creates an instance from a bounding {@link Path}.
    340      *
    341      * @hide
    342      */
    343     public static DisplayCutout fromBounds(Path path) {
    344         RectF clipRect = new RectF();
    345         path.computeBounds(clipRect, false /* unused */);
    346         Region clipRegion = Region.obtain();
    347         clipRegion.set((int) clipRect.left, (int) clipRect.top,
    348                 (int) clipRect.right, (int) clipRect.bottom);
    349 
    350         Region bounds = new Region();
    351         bounds.setPath(path, clipRegion);
    352         clipRegion.recycle();
    353         return new DisplayCutout(ZERO_RECT, bounds, false /* copyArguments */);
    354     }
    355 
    356     /**
    357      * Creates the bounding path according to @android:string/config_mainBuiltInDisplayCutout.
    358      *
    359      * @hide
    360      */
    361     public static DisplayCutout fromResources(Resources res, int displayWidth, int displayHeight) {
    362         return fromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout),
    363                 displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT);
    364     }
    365 
    366     /**
    367      * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout.
    368      *
    369      * @hide
    370      */
    371     public static Path pathFromResources(Resources res, int displayWidth, int displayHeight) {
    372         return pathAndDisplayCutoutFromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout),
    373                 displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT).first;
    374     }
    375 
    376     /**
    377      * Creates an instance according to the supplied {@link android.util.PathParser.PathData} spec.
    378      *
    379      * @hide
    380      */
    381     @VisibleForTesting(visibility = PRIVATE)
    382     public static DisplayCutout fromSpec(String spec, int displayWidth, int displayHeight,
    383             float density) {
    384         return pathAndDisplayCutoutFromSpec(spec, displayWidth, displayHeight, density).second;
    385     }
    386 
    387     private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec(String spec,
    388             int displayWidth, int displayHeight, float density) {
    389         if (TextUtils.isEmpty(spec)) {
    390             return NULL_PAIR;
    391         }
    392         synchronized (CACHE_LOCK) {
    393             if (spec.equals(sCachedSpec) && sCachedDisplayWidth == displayWidth
    394                     && sCachedDisplayHeight == displayHeight
    395                     && sCachedDensity == density) {
    396                 return sCachedCutout;
    397             }
    398         }
    399         spec = spec.trim();
    400         final float offsetX;
    401         if (spec.endsWith(RIGHT_MARKER)) {
    402             offsetX = displayWidth;
    403             spec = spec.substring(0, spec.length() - RIGHT_MARKER.length()).trim();
    404         } else {
    405             offsetX = displayWidth / 2f;
    406         }
    407         final boolean inDp = spec.endsWith(DP_MARKER);
    408         if (inDp) {
    409             spec = spec.substring(0, spec.length() - DP_MARKER.length());
    410         }
    411 
    412         String bottomSpec = null;
    413         if (spec.contains(BOTTOM_MARKER)) {
    414             String[] splits = spec.split(BOTTOM_MARKER, 2);
    415             spec = splits[0].trim();
    416             bottomSpec = splits[1].trim();
    417         }
    418 
    419         final Path p;
    420         try {
    421             p = PathParser.createPathFromPathData(spec);
    422         } catch (Throwable e) {
    423             Log.wtf(TAG, "Could not inflate cutout: ", e);
    424             return NULL_PAIR;
    425         }
    426 
    427         final Matrix m = new Matrix();
    428         if (inDp) {
    429             m.postScale(density, density);
    430         }
    431         m.postTranslate(offsetX, 0);
    432         p.transform(m);
    433 
    434         if (bottomSpec != null) {
    435             final Path bottomPath;
    436             try {
    437                 bottomPath = PathParser.createPathFromPathData(bottomSpec);
    438             } catch (Throwable e) {
    439                 Log.wtf(TAG, "Could not inflate bottom cutout: ", e);
    440                 return NULL_PAIR;
    441             }
    442             // Keep top transform
    443             m.postTranslate(0, displayHeight);
    444             bottomPath.transform(m);
    445             p.addPath(bottomPath);
    446         }
    447 
    448         final Pair<Path, DisplayCutout> result = new Pair<>(p, fromBounds(p));
    449         synchronized (CACHE_LOCK) {
    450             sCachedSpec = spec;
    451             sCachedDisplayWidth = displayWidth;
    452             sCachedDisplayHeight = displayHeight;
    453             sCachedDensity = density;
    454             sCachedCutout = result;
    455         }
    456         return result;
    457     }
    458 
    459     private static Region boundingRectsToRegion(List<Rect> rects) {
    460         Region result = Region.obtain();
    461         if (rects != null) {
    462             for (Rect r : rects) {
    463                 result.op(r, Region.Op.UNION);
    464             }
    465         }
    466         return result;
    467     }
    468 
    469     /**
    470      * Helper class for passing {@link DisplayCutout} through binder.
    471      *
    472      * Needed, because {@code readFromParcel} cannot be used with immutable classes.
    473      *
    474      * @hide
    475      */
    476     public static final class ParcelableWrapper implements Parcelable {
    477 
    478         private DisplayCutout mInner;
    479 
    480         public ParcelableWrapper() {
    481             this(NO_CUTOUT);
    482         }
    483 
    484         public ParcelableWrapper(DisplayCutout cutout) {
    485             mInner = cutout;
    486         }
    487 
    488         @Override
    489         public int describeContents() {
    490             return 0;
    491         }
    492 
    493         @Override
    494         public void writeToParcel(Parcel out, int flags) {
    495             writeCutoutToParcel(mInner, out, flags);
    496         }
    497 
    498         /**
    499          * Writes a DisplayCutout to a {@link Parcel}.
    500          *
    501          * @see #readCutoutFromParcel(Parcel)
    502          */
    503         public static void writeCutoutToParcel(DisplayCutout cutout, Parcel out, int flags) {
    504             if (cutout == null) {
    505                 out.writeInt(-1);
    506             } else if (cutout == NO_CUTOUT) {
    507                 out.writeInt(0);
    508             } else {
    509                 out.writeInt(1);
    510                 out.writeTypedObject(cutout.mSafeInsets, flags);
    511                 out.writeTypedObject(cutout.mBounds, flags);
    512             }
    513         }
    514 
    515         /**
    516          * Similar to {@link Creator#createFromParcel(Parcel)}, but reads into an existing
    517          * instance.
    518          *
    519          * Needed for AIDL out parameters.
    520          */
    521         public void readFromParcel(Parcel in) {
    522             mInner = readCutoutFromParcel(in);
    523         }
    524 
    525         public static final Creator<ParcelableWrapper> CREATOR = new Creator<ParcelableWrapper>() {
    526             @Override
    527             public ParcelableWrapper createFromParcel(Parcel in) {
    528                 return new ParcelableWrapper(readCutoutFromParcel(in));
    529             }
    530 
    531             @Override
    532             public ParcelableWrapper[] newArray(int size) {
    533                 return new ParcelableWrapper[size];
    534             }
    535         };
    536 
    537         /**
    538          * Reads a DisplayCutout from a {@link Parcel}.
    539          *
    540          * @see #writeCutoutToParcel(DisplayCutout, Parcel, int)
    541          */
    542         public static DisplayCutout readCutoutFromParcel(Parcel in) {
    543             int variant = in.readInt();
    544             if (variant == -1) {
    545                 return null;
    546             }
    547             if (variant == 0) {
    548                 return NO_CUTOUT;
    549             }
    550 
    551             Rect safeInsets = in.readTypedObject(Rect.CREATOR);
    552             Region bounds = in.readTypedObject(Region.CREATOR);
    553 
    554             return new DisplayCutout(safeInsets, bounds, false /* copyArguments */);
    555         }
    556 
    557         public DisplayCutout get() {
    558             return mInner;
    559         }
    560 
    561         public void set(ParcelableWrapper cutout) {
    562             mInner = cutout.get();
    563         }
    564 
    565         public void set(DisplayCutout cutout) {
    566             mInner = cutout;
    567         }
    568 
    569         @Override
    570         public int hashCode() {
    571             return mInner.hashCode();
    572         }
    573 
    574         @Override
    575         public boolean equals(Object o) {
    576             return o instanceof ParcelableWrapper
    577                     && mInner.equals(((ParcelableWrapper) o).mInner);
    578         }
    579 
    580         @Override
    581         public String toString() {
    582             return String.valueOf(mInner);
    583         }
    584     }
    585 }
    586