content: /*
   * Copyright (C) 2015 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 com.android.launcher3.allapps;
  
  import android.content.Context;
  import android.content.Intent;
  import android.content.res.Resources;
  import android.support.animation.DynamicAnimation;
  import android.support.animation.SpringAnimation;
  import android.support.v4.view.accessibility.AccessibilityEventCompat;
  import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
  import android.support.v4.view.accessibility.AccessibilityRecordCompat;
  import android.support.v7.widget.GridLayoutManager;
  import android.support.v7.widget.RecyclerView;
  import android.view.Gravity;
  import android.view.LayoutInflater;
  import android.view.View;
  import android.view.View.OnFocusChangeListener;
  import android.view.ViewConfiguration;
  import android.view.ViewGroup;
  import android.view.accessibility.AccessibilityEvent;
  import android.widget.TextView;
  
  import com.android.launcher3.AppInfo;
  import com.android.launcher3.BubbleTextView;
  import com.android.launcher3.Launcher;
  import com.android.launcher3.R;
  import com.android.launcher3.Utilities;
  import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
  import com.android.launcher3.anim.SpringAnimationHandler;
  import com.android.launcher3.config.FeatureFlags;
  import com.android.launcher3.discovery.AppDiscoveryAppInfo;
  import com.android.launcher3.discovery.AppDiscoveryItemView;
  import com.android.launcher3.util.PackageManagerHelper;
  
  import java.util.List;
  
  /**
   * The grid view adapter of all the apps.
   */
  public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> {
  
      public static final String TAG = "AppsGridAdapter";
  
      // A normal icon
      public static final int VIEW_TYPE_ICON = 1 << 1;
      // A prediction icon
      public static final int VIEW_TYPE_PREDICTION_ICON = 1 << 2;
      // The message shown when there are no filtered results
      public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 3;
      // The message to continue to a market search when there are no filtered results
      public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 4;
  
      // We use various dividers for various purposes.  They share enough attributes to reuse layouts,
      // but differ in enough attributes to require different view types
  
      // A divider that separates the apps list and the search market button
      public static final int VIEW_TYPE_SEARCH_MARKET_DIVIDER = 1 << 5;
      // The divider that separates prediction icons from the app list
      public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 6;
      public static final int VIEW_TYPE_APPS_LOADING_DIVIDER = 1 << 7;
      public static final int VIEW_TYPE_DISCOVERY_ITEM = 1 << 8;
  
      // Common view type masks
      public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_MARKET_DIVIDER
              | VIEW_TYPE_PREDICTION_DIVIDER;
      public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON
              | VIEW_TYPE_PREDICTION_ICON;
      public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON
              | VIEW_TYPE_DISCOVERY_ITEM;
      public static final int VIEW_TYPE_MASK_HAS_SPRINGS = VIEW_TYPE_MASK_ICON
              | VIEW_TYPE_PREDICTION_DIVIDER;
  
  
      public interface BindViewCallback {
          void onBindView(ViewHolder holder);
      }
  
      /**
       * ViewHolder for each icon.
       */
      public static class ViewHolder extends RecyclerView.ViewHolder {
  
          public ViewHolder(View v) {
              super(v);
          }
      }
  
      /**
       * A subclass of GridLayoutManager that overrides accessibility values during app search.
       */
      public class AppsGridLayoutManager extends GridLayoutManager {
  
          public AppsGridLayoutManager(Context context) {
              super(context, 1, GridLayoutManager.VERTICAL, false);
          }
  
          @Override
          public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
              super.onInitializeAccessibilityEvent(event);
  
              // Ensure that we only report the number apps for accessibility not including other
              // adapter views
              final AccessibilityRecordCompat record = AccessibilityEventCompat
                      .asRecord(event);
              record.setItemCount(mApps.getNumFilteredApps());
              record.setFromIndex(Math.max(0,
                      record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex())));
              record.setToIndex(Math.max(0,
                      record.getToIndex() - getRowsNotForAccessibility(record.getToIndex())));
          }
  
          @Override
          public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
                  RecyclerView.State state) {
              return super.getRowCountForAccessibility(recycler, state) -
                      getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1);
          }
  
          @Override
          public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
                  RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
              super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info);
  
              ViewGroup.LayoutParams lp = host.getLayoutParams();
              AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo();
              if (!(lp instanceof LayoutParams) || (cic == null)) {
                  return;
              }
              LayoutParams glp = (LayoutParams) lp;
              info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
                      cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()),
                      cic.getRowSpan(),
                      cic.getColumnIndex(),
                      cic.getColumnSpan(),
                      cic.isHeading(),
                      cic.isSelected()));
          }
  
          /**
           * Returns the number of rows before {@param adapterPosition}, including this position
           * which should not be counted towards the collection info.
           */
          private int getRowsNotForAccessibility(int adapterPosition) {
              List<AdapterItem> items = mApps.getAdapterItems();
              adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1);
              int extraRows = 0;
              for (int i = 0; i <= adapterPosition; i++) {
                  if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_CONTENT)) {
                      extraRows++;
                  }
              }
              return extraRows;
          }
      }
  
      /**
       * Helper class to size the grid items.
       */
      public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
  
          public GridSpanSizer() {
              super();
              setSpanIndexCacheEnabled(true);
          }
  
          @Override
          public int getSpanSize(int position) {
              if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) {
                  return 1;
              } else {
                      // Section breaks span the full width
                      return mAppsPerRow;
              }
          }
      }
  
      private final Launcher mLauncher;
      private final LayoutInflater mLayoutInflater;
      private final AlphabeticalAppsList mApps;
      private final GridLayoutManager mGridLayoutMgr;
      private final GridSpanSizer mGridSizer;
      private final View.OnClickListener mIconClickListener;
      private final View.OnLongClickListener mIconLongClickListener;
  
      private int mAppsPerRow;
  
      private BindViewCallback mBindViewCallback;
      private OnFocusChangeListener mIconFocusListener;
  
      // The text to show when there are no search results and no market search handler.
      private String mEmptySearchMessage;
      // The intent to send off to the market app, updated each time the search query changes.
      private Intent mMarketSearchIntent;
  
      private SpringAnimationHandler<ViewHolder> mSpringAnimationHandler;
  
      public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener
              iconClickListener, View.OnLongClickListener iconLongClickListener) {
          Resources res = launcher.getResources();
          mLauncher = launcher;
          mApps = apps;
          mEmptySearchMessage = res.getString(R.string.all_apps_loading_message);
          mGridSizer = new GridSpanSizer();
          mGridLayoutMgr = new AppsGridLayoutManager(launcher);
          mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
          mLayoutInflater = LayoutInflater.from(launcher);
          mIconClickListener = iconClickListener;
          mIconLongClickListener = iconLongClickListener;
          if (FeatureFlags.LAUNCHER3_PHYSICS) {
              mSpringAnimationHandler = new SpringAnimationHandler<>(
                      SpringAnimationHandler.Y_DIRECTION, new AllAppsSpringAnimationFactory());
          }
      }
  
      public SpringAnimationHandler getSpringAnimationHandler() {
          return mSpringAnimationHandler;
      }
  
      public static boolean isDividerViewType(int viewType) {
          return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER);
      }
  
      public static boolean isIconViewType(int viewType) {
          return isViewType(viewType, VIEW_TYPE_MASK_ICON);
      }
  
      public static boolean isViewType(int viewType, int viewTypeMask) {
          return (viewType & viewTypeMask) != 0;
      }
  
      /**
       * Sets the number of apps per row.
       */
      public void setNumAppsPerRow(int appsPerRow) {
          mAppsPerRow = appsPerRow;
          mGridLayoutMgr.setSpanCount(appsPerRow);
      }
  
      public int getNumAppsPerRow() {
          return mAppsPerRow;
      }
  
      public void setIconFocusListener(OnFocusChangeListener focusListener) {
          mIconFocusListener = focusListener;
      }
  
      /**
       * Sets the last search query that was made, used to show when there are no results and to also
       * seed the intent for searching the market.
       */
      public void setLastSearchQuery(String query) {
          Resources res = mLauncher.getResources();
          mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query);
          mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query);
      }
  
      /**
       * Sets the callback for when views are bound.
       */
      public void setBindViewCallback(BindViewCallback cb) {
          mBindViewCallback = cb;
      }
  
      /**
       * Returns the grid layout manager.
       */
      public GridLayoutManager getLayoutManager() {
          return mGridLayoutMgr;
      }
  
      @Override
      public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
          switch (viewType) {
              case VIEW_TYPE_ICON:
              case VIEW_TYPE_PREDICTION_ICON:
                  BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
                          R.layout.all_apps_icon, parent, false);
                  icon.setOnClickListener(mIconClickListener);
                  icon.setOnLongClickListener(mIconLongClickListener);
                  icon.setLongPressTimeout(ViewConfiguration.getLongPressTimeout());
                  icon.setOnFocusChangeListener(mIconFocusListener);
  
                  // Ensure the all apps icon height matches the workspace icons in portrait mode.
                  icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx;
                  return new ViewHolder(icon);
              case VIEW_TYPE_DISCOVERY_ITEM:
                  AppDiscoveryItemView appDiscoveryItemView = (AppDiscoveryItemView) mLayoutInflater
                          .inflate(R.layout.all_apps_discovery_item, parent, false);
                  appDiscoveryItemView.init(mIconClickListener, mLauncher.getAccessibilityDelegate(),
                          mIconLongClickListener);
                  return new ViewHolder(appDiscoveryItemView);
              case VIEW_TYPE_EMPTY_SEARCH:
                  return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
                          parent, false));
              case VIEW_TYPE_SEARCH_MARKET:
                  View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
                          parent, false);
                  searchMarketView.setOnClickListener(new View.OnClickListener() {
                      @Override
                      public void onClick(View v) {
                          mLauncher.startActivitySafely(v, mMarketSearchIntent, null);
                      }
                  });
                  return new ViewHolder(searchMarketView);
              case VIEW_TYPE_APPS_LOADING_DIVIDER:
                  View loadingDividerView = mLayoutInflater.inflate(
                          R.layout.all_apps_discovery_loading_divider, parent, false);
                  return new ViewHolder(loadingDividerView);
              case VIEW_TYPE_PREDICTION_DIVIDER:
              case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
                  return new ViewHolder(mLayoutInflater.inflate(
                          R.layout.all_apps_divider, parent, false));
              default:
                  throw new RuntimeException("Unexpected view type");
          }
      }
  
      @Override
      public void onBindViewHolder(ViewHolder holder, int position) {
          switch (holder.getItemViewType()) {
              case VIEW_TYPE_ICON:
              case VIEW_TYPE_PREDICTION_ICON:
                  AppInfo info = mApps.getAdapterItems().get(position).appInfo;
                  BubbleTextView icon = (BubbleTextView) holder.itemView;
                  icon.applyFromApplicationInfo(info);
                  break;
              case VIEW_TYPE_DISCOVERY_ITEM:
                  AppDiscoveryAppInfo appDiscoveryAppInfo = (AppDiscoveryAppInfo)
                          mApps.getAdapterItems().get(position).appInfo;
                  AppDiscoveryItemView view = (AppDiscoveryItemView) holder.itemView;
                  view.apply(appDiscoveryAppInfo);
                  break;
              case VIEW_TYPE_EMPTY_SEARCH:
                  TextView emptyViewText = (TextView) holder.itemView;
                  emptyViewText.setText(mEmptySearchMessage);
                  emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
                          Gravity.START | Gravity.CENTER_VERTICAL);
                  break;
              case VIEW_TYPE_SEARCH_MARKET:
                  TextView searchView = (TextView) holder.itemView;
                  if (mMarketSearchIntent != null) {
                      searchView.setVisibility(View.VISIBLE);
                  } else {
                      searchView.setVisibility(View.GONE);
                  }
                  break;
              case VIEW_TYPE_APPS_LOADING_DIVIDER:
                  int visLoading = mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE;
                  int visLoaded = !mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE;
                  holder.itemView.findViewById(R.id.loadingProgressBar).setVisibility(visLoading);
                  holder.itemView.findViewById(R.id.loadedDivider).setVisibility(visLoaded);
                  break;
              case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
                  // nothing to do
                  break;
          }
          if (mBindViewCallback != null) {
              mBindViewCallback.onBindView(holder);
          }
      }
  
      @Override
      public void onViewAttachedToWindow(ViewHolder holder) {
          int type = holder.getItemViewType();
          if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
              mSpringAnimationHandler.add(holder.itemView, holder);
          }
      }
  
      @Override
      public void onViewDetachedFromWindow(ViewHolder holder) {
          int type = holder.getItemViewType();
          if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
              mSpringAnimationHandler.remove(holder.itemView);
          }
      }
  
      @Override
      public boolean onFailedToRecycleView(ViewHolder holder) {
          // Always recycle and we will reset the view when it is bound
          return true;
      }
  
      @Override
      public int getItemCount() {
          return mApps.getAdapterItems().size();
      }
  
      @Override
      public int getItemViewType(int position) {
          AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
          return item.viewType;
      }
  
      /**
       * Helper class to set the SpringAnimation values for an item in the adapter.
       */
      private class AllAppsSpringAnimationFactory
              implements SpringAnimationHandler.AnimationFactory<ViewHolder> {
          private static final float DEFAULT_MAX_VALUE_PX = 100;
          private static final float DEFAULT_MIN_VALUE_PX = -DEFAULT_MAX_VALUE_PX;
  
          // Damping ratio range is [0, 1]
          private static final float SPRING_DAMPING_RATIO = 0.55f;
  
          // Stiffness is a non-negative number.
          private static final float MIN_SPRING_STIFFNESS = 580f;
          private static final float MAX_SPRING_STIFFNESS = 900f;
  
          // The amount by which each adjacent rows' stiffness will differ.
          private static final float ROW_STIFFNESS_COEFFICIENT = 50f;
  
          @Override
          public SpringAnimation initialize(ViewHolder vh) {
              return SpringAnimationHandler.forView(vh.itemView, DynamicAnimation.TRANSLATION_Y, 0);
          }
  
          /**
           * @param spring A new or recycled SpringAnimation.
           * @param vh The ViewHolder that {@param spring} is related to.
           */
          @Override
          public void update(SpringAnimation spring, ViewHolder vh) {
              int numPredictedApps = Math.min(mAppsPerRow, mApps.getPredictedApps().size());
              int appPosition = getAppPosition(vh.getAdapterPosition(), numPredictedApps,
                      mAppsPerRow);
  
              int col = appPosition % mAppsPerRow;
              int row = appPosition / mAppsPerRow;
  
              int numTotalRows = mApps.getNumAppRows() - 1; // zero-based count
              if (row > (numTotalRows / 2)) {
                  // Mirror the rows so that the top row acts the same as the bottom row.
                  row = Math.abs(numTotalRows - row);
              }
  
              calculateSpringValues(spring, row, col);
          }
  
          @Override
          public void setDefaultValues(SpringAnimation spring) {
              calculateSpringValues(spring, 0, mAppsPerRow / 2);
          }
  
          /**
           * We manipulate the stiffness, min, and max values based on the items distance to the
           * first row and the items distance to the center column to create the ^-shaped motion
           * effect.
           */
          private void calculateSpringValues(SpringAnimation spring, int row, int col) {
              float rowFactor = (1 + row) * 0.5f;
              float colFactor = getColumnFactor(col, mAppsPerRow);
  
              float minValue = DEFAULT_MIN_VALUE_PX * (rowFactor + colFactor);
              float maxValue = DEFAULT_MAX_VALUE_PX * (rowFactor + colFactor);
  
              float stiffness = Utilities.boundToRange(
                      MAX_SPRING_STIFFNESS - (row * ROW_STIFFNESS_COEFFICIENT),
                      MIN_SPRING_STIFFNESS,
                      MAX_SPRING_STIFFNESS);
  
              spring.setMinValue(minValue)
                      .setMaxValue(maxValue)
                      .getSpring()
                      .setStiffness(stiffness)
                      .setDampingRatio(SPRING_DAMPING_RATIO);
          }
  
          /**
           * @return The app position is the position of the app in the Adapter if we ignored all
           * other view types.
           *
           * The first app is at position 0, and the first app each following row is at a
           * position that is a multiple of {@param appsPerRow}.
           *
           * ie. If there are 5 apps per row, and there are two rows of apps:
           *     0 1 2 3 4
           *     5 6 7 8 9
           */
          private int getAppPosition(int position, int numPredictedApps, int appsPerRow) {
              if (position < numPredictedApps) {
                  // Predicted apps are first in the adapter.
                  return position;
              }
  
              // There is at most 1 divider view between the predicted apps and the alphabetical apps.
              int numDividerViews = numPredictedApps == 0 ? 0 : 1;
  
              // This offset takes into consideration an incomplete row of predicted apps.
              int numPredictedAppsOffset = appsPerRow - numPredictedApps;
              return position + numPredictedAppsOffset - numDividerViews;
          }
  
          /**
           * Increase the column factor as the distance increases between the column and the center
           * column(s).
           */
          private float getColumnFactor(int col, int numCols) {
              float centerColumn = numCols / 2;
              int distanceToCenter = (int) Math.abs(col - centerColumn);
  
              boolean evenNumberOfColumns = numCols % 2 == 0;
              if (evenNumberOfColumns && col < centerColumn) {
                  distanceToCenter -= 1;
              }
  
              float factor = 0;
              while (distanceToCenter > 0) {
                  if (distanceToCenter == 1) {
                      factor += 0.2f;
                  } else {
                      factor += 0.1f;
                  }
                  --distanceToCenter;
              }
  
              return factor;
          }
      }
  }
  