content: /*
   * Copyright (C) 2018 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.view;
  
  import static android.view.ViewRootImpl.NEW_INSETS_MODE_FULL;
  import static android.view.ViewRootImpl.NEW_INSETS_MODE_NONE;
  import static android.view.WindowInsets.Type.MANDATORY_SYSTEM_GESTURES;
  import static android.view.WindowInsets.Type.SIZE;
  import static android.view.WindowInsets.Type.SYSTEM_GESTURES;
  import static android.view.WindowInsets.Type.indexOf;
  
  import android.annotation.IntDef;
  import android.annotation.Nullable;
  import android.graphics.Insets;
  import android.graphics.Rect;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.SparseIntArray;
  import android.view.WindowInsets.Type;
  import android.view.WindowInsets.Type.InsetType;
  import android.view.WindowManager.LayoutParams;
  
  import java.io.PrintWriter;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.Objects;
  
  /**
   * Holder for state of system windows that cause window insets for all other windows in the system.
   * @hide
   */
  public class InsetsState implements Parcelable {
  
      /**
       * Internal representation of inset source types. This is different from the public API in
       * {@link WindowInsets.Type} as one type from the public API might indicate multiple windows
       * at the same time.
       */
      @Retention(RetentionPolicy.SOURCE)
      @IntDef(prefix = "TYPE", value = {
              TYPE_TOP_BAR,
              TYPE_SIDE_BAR_1,
              TYPE_SIDE_BAR_2,
              TYPE_SIDE_BAR_3,
              TYPE_TOP_GESTURES,
              TYPE_BOTTOM_GESTURES,
              TYPE_LEFT_GESTURES,
              TYPE_RIGHT_GESTURES,
              TYPE_TOP_TAPPABLE_ELEMENT,
              TYPE_BOTTOM_TAPPABLE_ELEMENT,
              TYPE_IME
      })
      public @interface InternalInsetType {}
  
      static final int FIRST_TYPE = 0;
  
      /** Top bar. Can be status bar or caption in freeform windowing mode. */
      public static final int TYPE_TOP_BAR = FIRST_TYPE;
  
      /**
       * Up to 3 side bars that appear on left/right/bottom. On phones there is only one side bar
       * (the navigation bar, see {@link #TYPE_NAVIGATION_BAR}), but other form factors might have
       * multiple, like Android Auto.
       */
      public static final int TYPE_SIDE_BAR_1 = 1;
      public static final int TYPE_SIDE_BAR_2 = 2;
      public static final int TYPE_SIDE_BAR_3 = 3;
  
      public static final int TYPE_TOP_GESTURES = 4;
      public static final int TYPE_BOTTOM_GESTURES = 5;
      public static final int TYPE_LEFT_GESTURES = 6;
      public static final int TYPE_RIGHT_GESTURES = 7;
      public static final int TYPE_TOP_TAPPABLE_ELEMENT = 8;
      public static final int TYPE_BOTTOM_TAPPABLE_ELEMENT = 9;
  
      /** Input method window. */
      public static final int TYPE_IME = 10;
  
      static final int LAST_TYPE = TYPE_IME;
  
      // Derived types
  
      /** First side bar is navigation bar. */
      public static final int TYPE_NAVIGATION_BAR = TYPE_SIDE_BAR_1;
  
      /** A shelf is the same as the navigation bar. */
      public static final int TYPE_SHELF = TYPE_NAVIGATION_BAR;
  
      @Retention(RetentionPolicy.SOURCE)
      @IntDef(prefix = "INSET_SIDE", value = {
              INSET_SIDE_LEFT,
              INSET_SIDE_TOP,
              INSET_SIDE_RIGHT,
              INSET_SIDE_BOTTOM,
              INSET_SIDE_UNKNWON
      })
      public @interface InsetSide {}
      static final int INSET_SIDE_LEFT = 0;
      static final int INSET_SIDE_TOP = 1;
      static final int INSET_SIDE_RIGHT = 2;
      static final int INSET_SIDE_BOTTOM = 3;
      static final int INSET_SIDE_UNKNWON = 4;
  
      private final ArrayMap<Integer, InsetsSource> mSources = new ArrayMap<>();
  
      /**
       * The frame of the display these sources are relative to.
       */
      private final Rect mDisplayFrame = new Rect();
  
      public InsetsState() {
      }
  
      public InsetsState(InsetsState copy) {
          set(copy);
      }
  
      public InsetsState(InsetsState copy, boolean copySources) {
          set(copy, copySources);
      }
  
      /**
       * Calculates {@link WindowInsets} based on the current source configuration.
       *
       * @param frame The frame to calculate the insets relative to.
       * @return The calculated insets.
       */
      public WindowInsets calculateInsets(Rect frame, boolean isScreenRound,
              boolean alwaysConsumeSystemBars, DisplayCutout cutout,
              @Nullable Rect legacyContentInsets, @Nullable Rect legacyStableInsets,
              int legacySoftInputMode, @Nullable @InsetSide SparseIntArray typeSideMap) {
          Insets[] typeInsetsMap = new Insets[Type.SIZE];
          Insets[] typeMaxInsetsMap = new Insets[Type.SIZE];
          boolean[] typeVisibilityMap = new boolean[SIZE];
          final Rect relativeFrame = new Rect(frame);
          final Rect relativeFrameMax = new Rect(frame);
          if (ViewRootImpl.sNewInsetsMode != NEW_INSETS_MODE_FULL
                  && legacyContentInsets != null && legacyStableInsets != null) {
              WindowInsets.assignCompatInsets(typeInsetsMap, legacyContentInsets);
              WindowInsets.assignCompatInsets(typeMaxInsetsMap, legacyStableInsets);
          }
          for (int type = FIRST_TYPE; type <= LAST_TYPE; type++) {
              InsetsSource source = mSources.get(type);
              if (source == null) {
                  continue;
              }
  
              boolean skipSystemBars = ViewRootImpl.sNewInsetsMode != NEW_INSETS_MODE_FULL
                      && (type == TYPE_TOP_BAR || type == TYPE_NAVIGATION_BAR);
              boolean skipIme = source.getType() == TYPE_IME
                      && (legacySoftInputMode & LayoutParams.SOFT_INPUT_ADJUST_RESIZE) == 0;
              boolean skipLegacyTypes = ViewRootImpl.sNewInsetsMode == NEW_INSETS_MODE_NONE
                      && (toPublicType(type) & Type.compatSystemInsets()) != 0;
              if (skipSystemBars || skipIme || skipLegacyTypes) {
                  typeVisibilityMap[indexOf(toPublicType(type))] = source.isVisible();
                  continue;
              }
  
              processSource(source, relativeFrame, false /* ignoreVisibility */, typeInsetsMap,
                      typeSideMap, typeVisibilityMap);
  
              // IME won't be reported in max insets as the size depends on the EditorInfo of the IME
              // target.
              if (source.getType() != TYPE_IME) {
                  processSource(source, relativeFrameMax, true /* ignoreVisibility */,
                          typeMaxInsetsMap, null /* typeSideMap */, null /* typeVisibilityMap */);
              }
          }
          return new WindowInsets(typeInsetsMap, typeMaxInsetsMap, typeVisibilityMap, isScreenRound,
                  alwaysConsumeSystemBars, cutout);
      }
  
      private void processSource(InsetsSource source, Rect relativeFrame, boolean ignoreVisibility,
              Insets[] typeInsetsMap, @Nullable @InsetSide SparseIntArray typeSideMap,
              @Nullable boolean[] typeVisibilityMap) {
          Insets insets = source.calculateInsets(relativeFrame, ignoreVisibility);
  
          int type = toPublicType(source.getType());
          processSourceAsPublicType(source, typeInsetsMap, typeSideMap, typeVisibilityMap,
                  insets, type);
  
          if (type == MANDATORY_SYSTEM_GESTURES) {
              // Mandatory system gestures are also system gestures.
              // TODO: find a way to express this more generally. One option would be to define
              //       Type.systemGestureInsets() as NORMAL | MANDATORY, but then we lose the
              //       ability to set systemGestureInsets() independently from
              //       mandatorySystemGestureInsets() in the Builder.
              processSourceAsPublicType(source, typeInsetsMap, typeSideMap, typeVisibilityMap,
                      insets, SYSTEM_GESTURES);
          }
      }
  
      private void processSourceAsPublicType(InsetsSource source, Insets[] typeInsetsMap,
              @InsetSide @Nullable SparseIntArray typeSideMap,
              @Nullable boolean[] typeVisibilityMap, Insets insets, int type) {
          int index = indexOf(type);
          Insets existing = typeInsetsMap[index];
          if (existing == null) {
              typeInsetsMap[index] = insets;
          } else {
              typeInsetsMap[index] = Insets.max(existing, insets);
          }
  
          if (typeVisibilityMap != null) {
              typeVisibilityMap[index] = source.isVisible();
          }
  
          if (typeSideMap != null && !Insets.NONE.equals(insets)) {
              @InsetSide int insetSide = getInsetSide(insets);
              if (insetSide != INSET_SIDE_UNKNWON) {
                  typeSideMap.put(source.getType(), getInsetSide(insets));
              }
          }
      }
  
      /**
       * Retrieves the side for a certain {@code insets}. It is required that only one field l/t/r/b
       * is set in order that this method returns a meaningful result.
       */
      private @InsetSide int getInsetSide(Insets insets) {
          if (insets.left != 0) {
              return INSET_SIDE_LEFT;
          }
          if (insets.top != 0) {
              return INSET_SIDE_TOP;
          }
          if (insets.right != 0) {
              return INSET_SIDE_RIGHT;
          }
          if (insets.bottom != 0) {
              return INSET_SIDE_BOTTOM;
          }
          return INSET_SIDE_UNKNWON;
      }
  
      public InsetsSource getSource(@InternalInsetType int type) {
          return mSources.computeIfAbsent(type, InsetsSource::new);
      }
  
      public void setDisplayFrame(Rect frame) {
          mDisplayFrame.set(frame);
      }
  
      public Rect getDisplayFrame() {
          return mDisplayFrame;
      }
  
      /**
       * Modifies the state of this class to exclude a certain type to make it ready for dispatching
       * to the client.
       *
       * @param type The {@link InternalInsetType} of the source to remove
       */
      public void removeSource(int type) {
          mSources.remove(type);
      }
  
      public void set(InsetsState other) {
          set(other, false /* copySources */);
      }
  
      public void set(InsetsState other, boolean copySources) {
          mDisplayFrame.set(other.mDisplayFrame);
          mSources.clear();
          if (copySources) {
              for (int i = 0; i < other.mSources.size(); i++) {
                  InsetsSource source = other.mSources.valueAt(i);
                  mSources.put(source.getType(), new InsetsSource(source));
              }
          } else {
              mSources.putAll(other.mSources);
          }
      }
  
      public void addSource(InsetsSource source) {
          mSources.put(source.getType(), source);
      }
  
      public int getSourcesCount() {
          return mSources.size();
      }
  
      public InsetsSource sourceAt(int index) {
          return mSources.valueAt(index);
      }
  
      public static @InternalInsetType ArraySet<Integer> toInternalType(@InsetType int insetTypes) {
          final ArraySet<Integer> result = new ArraySet<>();
          if ((insetTypes & Type.TOP_BAR) != 0) {
              result.add(TYPE_TOP_BAR);
          }
          if ((insetTypes & Type.SIDE_BARS) != 0) {
              result.add(TYPE_SIDE_BAR_1);
              result.add(TYPE_SIDE_BAR_2);
              result.add(TYPE_SIDE_BAR_3);
          }
          if ((insetTypes & Type.IME) != 0) {
              result.add(TYPE_IME);
          }
          return result;
      }
  
      static @InsetType int toPublicType(@InternalInsetType int type) {
          switch (type) {
              case TYPE_TOP_BAR:
                  return Type.TOP_BAR;
              case TYPE_SIDE_BAR_1:
              case TYPE_SIDE_BAR_2:
              case TYPE_SIDE_BAR_3:
                  return Type.SIDE_BARS;
              case TYPE_IME:
                  return Type.IME;
              case TYPE_TOP_GESTURES:
              case TYPE_BOTTOM_GESTURES:
                  return Type.MANDATORY_SYSTEM_GESTURES;
              case TYPE_LEFT_GESTURES:
              case TYPE_RIGHT_GESTURES:
                  return Type.SYSTEM_GESTURES;
              case TYPE_TOP_TAPPABLE_ELEMENT:
              case TYPE_BOTTOM_TAPPABLE_ELEMENT:
                  return Type.TAPPABLE_ELEMENT;
              default:
                  throw new IllegalArgumentException("Unknown type: " + type);
          }
      }
  
      public static boolean getDefaultVisibility(@InsetType int type) {
          switch (type) {
              case TYPE_TOP_BAR:
              case TYPE_SIDE_BAR_1:
              case TYPE_SIDE_BAR_2:
              case TYPE_SIDE_BAR_3:
                  return true;
              case TYPE_IME:
                  return false;
              default:
                  return true;
          }
      }
  
      public void dump(String prefix, PrintWriter pw) {
          pw.println(prefix + "InsetsState");
          for (int i = mSources.size() - 1; i >= 0; i--) {
              mSources.valueAt(i).dump(prefix + "  ", pw);
          }
      }
  
      public static String typeToString(int type) {
          switch (type) {
              case TYPE_TOP_BAR:
                  return "TYPE_TOP_BAR";
              case TYPE_SIDE_BAR_1:
                  return "TYPE_SIDE_BAR_1";
              case TYPE_SIDE_BAR_2:
                  return "TYPE_SIDE_BAR_2";
              case TYPE_SIDE_BAR_3:
                  return "TYPE_SIDE_BAR_3";
              case TYPE_TOP_GESTURES:
                  return "TYPE_TOP_GESTURES";
              case TYPE_BOTTOM_GESTURES:
                  return "TYPE_BOTTOM_GESTURES";
              case TYPE_LEFT_GESTURES:
                  return "TYPE_LEFT_GESTURES";
              case TYPE_RIGHT_GESTURES:
                  return "TYPE_RIGHT_GESTURES";
              case TYPE_TOP_TAPPABLE_ELEMENT:
                  return "TYPE_TOP_TAPPABLE_ELEMENT";
              case TYPE_BOTTOM_TAPPABLE_ELEMENT:
                  return "TYPE_BOTTOM_TAPPABLE_ELEMENT";
              default:
                  return "TYPE_UNKNOWN_" + type;
          }
      }
  
      @Override
      public boolean equals(Object o) {
          if (this == o) { return true; }
          if (o == null || getClass() != o.getClass()) { return false; }
  
          InsetsState state = (InsetsState) o;
  
          if (!mDisplayFrame.equals(state.mDisplayFrame)) {
              return false;
          }
          if (mSources.size() != state.mSources.size()) {
              return false;
          }
          for (int i = mSources.size() - 1; i >= 0; i--) {
              InsetsSource source = mSources.valueAt(i);
              InsetsSource otherSource = state.mSources.get(source.getType());
              if (otherSource == null) {
                  return false;
              }
              if (!otherSource.equals(source)) {
                  return false;
              }
          }
          return true;
      }
  
      @Override
      public int hashCode() {
          return Objects.hash(mDisplayFrame, mSources);
      }
  
      public InsetsState(Parcel in) {
          readFromParcel(in);
      }
  
      @Override
      public int describeContents() {
          return 0;
      }
  
      @Override
      public void writeToParcel(Parcel dest, int flags) {
          dest.writeParcelable(mDisplayFrame, flags);
          dest.writeInt(mSources.size());
          for (int i = 0; i < mSources.size(); i++) {
              dest.writeParcelable(mSources.valueAt(i), flags);
          }
      }
  
      public static final @android.annotation.NonNull Creator<InsetsState> CREATOR = new Creator<InsetsState>() {
  
          public InsetsState createFromParcel(Parcel in) {
              return new InsetsState(in);
          }
  
          public InsetsState[] newArray(int size) {
              return new InsetsState[size];
          }
      };
  
      public void readFromParcel(Parcel in) {
          mSources.clear();
          mDisplayFrame.set(in.readParcelable(null /* loader */));
          final int size = in.readInt();
          for (int i = 0; i < size; i++) {
              final InsetsSource source = in.readParcelable(null /* loader */);
              mSources.put(source.getType(), source);
          }
      }
  }
  
  