content: /*
   * Copyright 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 com.prefabulated.bouncyball;
  
  import android.content.Intent;
  import android.content.SharedPreferences;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Trace;
  import android.preference.ListPreference;
  import android.preference.PreferenceManager;
  import android.text.Layout;
  import android.util.Log;
  import android.view.Choreographer;
  import android.view.Display;
  import android.view.Menu;
  import android.view.MenuInflater;
  import android.view.MenuItem;
  import android.view.Surface;
  import android.view.SurfaceHolder;
  import android.view.SurfaceView;
  import android.view.View;
  import android.view.WindowManager;
  import android.widget.GridLayout;
  import android.widget.LinearLayout;
  import android.widget.TextView;
  
  import java.util.ArrayDeque;
  import java.util.Locale;
  import java.util.Queue;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
  
  import androidx.appcompat.app.AppCompatActivity;
  import androidx.appcompat.widget.AppCompatTextView;
  
  public class OrbitActivity extends AppCompatActivity implements Choreographer.FrameCallback, SurfaceHolder.Callback {
  
      // Used to load the 'native-lib' library on application startup.
      static {
          System.loadLibrary("native-lib");
      }
  
      private static final long ONE_MS_IN_NS = 1000000;
      private static final long ONE_S_IN_NS = 1000 * ONE_MS_IN_NS;
  
      private static final String LOG_TAG = "OrbitActivity.java";
  
      private void configureGridCell(AppCompatTextView cell) {
          // A bunch of optimizations to reduce the cost of setText
          cell.setEllipsize(null);
          cell.setMaxLines(1);
          cell.setSingleLine(true);
          if (android.os.Build.VERSION.SDK_INT >= 23) {
              cell.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE);
              cell.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE);
          }
  
          cell.setTextAppearance(getApplicationContext(), R.style.InfoTextSmall);
          cell.setText("0");
      }
  
      private void buildChoreographerInfoGrid() {
          GridLayout infoGrid = findViewById(R.id.choreographer_info_grid);
  
          // Add the header row
          GridLayout.Spec headerRowSpec = GridLayout.spec(0);
          for (int column = 0; column < mChoreographerInfoGrid[0].length; ++column) {
              AppCompatTextView cell = new AppCompatTextView(getApplicationContext());
              GridLayout.Spec colSpec = GridLayout.spec(column, 1.0f);
              cell.setLayoutParams(new GridLayout.LayoutParams(headerRowSpec, colSpec));
              configureGridCell(cell);
  
              if (column == 0) {
                  cell.setText("");
              } else {
                  cell.setText(String.format(Locale.US, "%d", column - 1));
              }
              infoGrid.addView(cell);
              mChoreographerInfoGrid[0][column] = cell;
          }
  
          // Add the data rows
          for (int row = 1; row < mChoreographerInfoGrid.length; ++row) {
              GridLayout.Spec rowSpec = GridLayout.spec(row);
  
              for (int column = 0; column < mChoreographerInfoGrid[row].length; ++column) {
                  AppCompatTextView cell = new AppCompatTextView(getApplicationContext());
                  GridLayout.Spec colSpec = GridLayout.spec(column, 1.0f);
                  cell.setLayoutParams(new GridLayout.LayoutParams(rowSpec, colSpec));
                  cell.setTextAppearance(getApplicationContext(), R.style.InfoTextSmall);
                  configureGridCell(cell);
  
                  if (column == 0) {
                      switch (row) {
                          case 1:
                              cell.setText(R.string.grid_1s_arr);
                              break;
                          case 2:
                              cell.setText(R.string.grid_1s_ts);
                              break;
                          case 3:
                              cell.setText(R.string.grid_1m_arr);
                              break;
                          case 4:
                              cell.setText(R.string.grid_1m_ts);
                              break;
                      }
                  }
                  infoGrid.addView(cell);
                  mChoreographerInfoGrid[row][column] = cell;
              }
          }
  
          for (TextView[] row : mChoreographerInfoGrid) {
              for (TextView column : row) {
                  column.setWidth(infoGrid.getWidth() / infoGrid.getColumnCount());
              }
          }
      }
  
      private void buildSwappyStatsGrid() {
          GridLayout infoGrid = findViewById(R.id.swappy_stats_grid);
  
          // Add the header row
          GridLayout.Spec headerRowSpec = GridLayout.spec(0);
          for (int column = 0; column < mSwappyGrid[0].length; ++column) {
              AppCompatTextView cell = new AppCompatTextView(getApplicationContext());
              GridLayout.Spec colSpec = GridLayout.spec(column, 1.0f);
              cell.setLayoutParams(new GridLayout.LayoutParams(headerRowSpec, colSpec));
              configureGridCell(cell);
  
              if (column == 0) {
                  cell.setText("");
              } else {
                  cell.setText(String.format(Locale.US, "%d", column - 1));
              }
              infoGrid.addView(cell);
              mSwappyGrid[0][column] = cell;
          }
  
          // Add the data rows
          for (int row = 1; row < mSwappyGrid.length; ++row) {
              GridLayout.Spec rowSpec = GridLayout.spec(row);
  
              for (int column = 0; column < mSwappyGrid[row].length; ++column) {
                  AppCompatTextView cell = new AppCompatTextView(getApplicationContext());
                  GridLayout.Spec colSpec = GridLayout.spec(column, 1.0f);
                  cell.setLayoutParams(new GridLayout.LayoutParams(rowSpec, colSpec));
                  cell.setTextAppearance(getApplicationContext(), R.style.InfoTextSmall);
                  configureGridCell(cell);
  
                  if (column == 0) {
                      switch (row) {
                          case 1:
                              cell.setText(R.string.idle_frames);
                              break;
                          case 2:
                              cell.setText(R.string.late_frames);
                              break;
                          case 3:
                              cell.setText(R.string.offset_frames);
                              break;
                          case 4:
                              cell.setText(R.string.latency_frames);
                              break;
                      }
                  } else {
                      cell.setText("0%");
                  }
                  infoGrid.addView(cell);
                  mSwappyGrid[row][column] = cell;
              }
          }
  
          for (TextView[] row : mSwappyGrid) {
              for (TextView column : row) {
                  column.setWidth(infoGrid.getWidth() / infoGrid.getColumnCount());
              }
          }
      }
  
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_orbit);
  
          // Get display metrics
  
          WindowManager wm = getWindowManager();
          Display display = wm.getDefaultDisplay();
          float refreshRateHz = display.getRefreshRate();
          Log.i(LOG_TAG, String.format("Refresh rate: %.1f Hz", refreshRateHz));
          long refreshPeriodNanos = (long) (ONE_S_IN_NS / refreshRateHz);
          long appVsyncOffsetNanos = display.getAppVsyncOffsetNanos();
          long sfVsyncOffsetNanos = refreshPeriodNanos - (display.getPresentationDeadlineNanos() - ONE_MS_IN_NS);
  
          // Initialize UI
  
          SurfaceView surfaceView = findViewById(R.id.surface_view);
          surfaceView.getHolder().addCallback(this);
  
          mInfoOverlay = findViewById(R.id.info_overlay);
          mInfoOverlay.setBackgroundColor(0x80000000);
  
          buildChoreographerInfoGrid();
          buildSwappyStatsGrid();
  
          TextView appOffsetView = findViewById(R.id.app_offset);
          appOffsetView.setText(String.format(Locale.US, "App Offset: %.1f ms", appVsyncOffsetNanos / (float) ONE_MS_IN_NS));
          TextView sfOffsetView = findViewById(R.id.sf_offset);
          sfOffsetView.setText(String.format(Locale.US, "SF Offset: %.1f ms", sfVsyncOffsetNanos / (float) ONE_MS_IN_NS));
  
          // Initialize the native renderer
  
          nInit();
  
          nSetPreference("refresh_period", String.valueOf(refreshPeriodNanos));
      }
  
      private void infoOverlayToggle() {
          if (mInfoOverlay == null) {
              return;
          }
  
          mInfoOverlayEnabled = !mInfoOverlayEnabled;
          if (mInfoOverlayEnabled) {
              mInfoOverlay.setVisibility(View.VISIBLE);
              mInfoOverlayButton.setIcon(R.drawable.ic_info_solid_white_24dp);
          } else {
              mInfoOverlay.setVisibility(View.INVISIBLE);
              mInfoOverlayButton.setIcon(R.drawable.ic_info_outline_white_24dp);
          }
      }
  
      @Override
      public boolean onCreateOptionsMenu(Menu menu) {
          MenuInflater inflater = getMenuInflater();
          inflater.inflate(R.menu.menu_orbit, menu);
  
          mInfoOverlayButton = menu.findItem(R.id.info_overlay_button);
          if (mInfoOverlayButton != null) {
              mInfoOverlayButton.setOnMenuItemClickListener((MenuItem item) -> {
                  infoOverlayToggle();
                  return true;
              });
          }
  
          MenuItem settingsButton = menu.findItem(R.id.settings_item);
          if (settingsButton != null) {
              settingsButton.setOnMenuItemClickListener((MenuItem) -> {
                  Intent intent = new Intent();
                  intent.setClass(getApplicationContext(), SettingsActivity.class);
                  startActivity(intent);
                  return true;
              });
          }
  
          return true;
      }
  
      @Override
      protected void onStart() {
          super.onStart();
  
          SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
          for (String key : sharedPreferences.getAll().keySet()) {
              if (key.equals("use_affinity")) {
                  nSetPreference(key, sharedPreferences.getBoolean(key, true) ? "true" : "false");
                  continue;
              } else if (key.equals("hot_pocket")) {
                  nSetPreference(key, sharedPreferences.getBoolean(key, false) ? "true" : "false");
                  continue;
              } else if (key.equals("use_auto_swap_interval")) {
                  nSetAutoSwapInterval(sharedPreferences.getBoolean(key, true));
                  continue;
              } else if (key.equals("workload")) {
                  nSetWorkload(sharedPreferences.getInt(key, 0));
                  continue;
              }
              nSetPreference(key, sharedPreferences.getString(key, null));
          }
  
          mIsRunning = true;
          nStart();
          Choreographer.getInstance().postFrameCallback(this);
      }
  
      @Override
      protected void onStop() {
          super.onStop();
  
          mIsRunning = false;
          nStop();
          clearChoreographerInfo();
      }
  
      private int deltaToBin(long delta) {
          long framePeriodNanos = 16666666;
          return (int) ((delta + framePeriodNanos / 2) / framePeriodNanos);
      }
  
      private void clearChoreographerInfo() {
          for (int i = 0; i < CHOREOGRAPHER_INFO_BIN_COUNT; ++i) {
              mArrivalBinsLastSecond[i] = 0;
              mArrivalBinsLastMinute[i] = 0;
              mTimestampBinsLastSecond[i] = 0;
              mTimestampBinsLastMinute[i] = 0;
          }
          mArrivalBinQueue.clear();
          mTimestampBinQueue.clear();
      }
  
      private void updateChoregrapherInfoBin(int row, int bin, int value) {
          if (value == mLastChoreographerInfoValues[row - 1][bin]) {
              return;
          }
  
          mLastChoreographerInfoValues[row - 1][bin] = value;
          mChoreographerInfoGrid[row][bin + 1].setText(String.valueOf(value));
      }
  
      private void updateSwappyStatsBin(int row, int bin, int value) {
          if (value == mLastSwappyStatsValues[row - 1][bin]) {
              return;
          }
  
          mLastSwappyStatsValues[row - 1][bin] = value;
          mSwappyGrid[row][bin + 1].setText(String.valueOf(value) + "%");
      }
  
      private void dumpBins() {
          Trace.beginSection("dumpBins");
  
          Trace.beginSection("addToQueues");
          mArrivalBinQueue.add(mArrivalBinsLastSecond.clone());
          mTimestampBinQueue.add(mTimestampBinsLastSecond.clone());
          Trace.endSection();
  
          Trace.beginSection("updateMinuteBins");
          for (int bin = 0; bin < CHOREOGRAPHER_INFO_BIN_COUNT; ++bin) {
              mArrivalBinsLastMinute[bin] += mArrivalBinsLastSecond[bin];
              mTimestampBinsLastMinute[bin] += mTimestampBinsLastSecond[bin];
          }
          Trace.endSection();
  
          Trace.beginSection("pruneQueue");
          if (mArrivalBinQueue.size() > 60) {
              int[] oldestArrivalBin = mArrivalBinQueue.remove();
              int[] oldestTimestampBin = mTimestampBinQueue.remove();
              for (int bin = 0; bin < CHOREOGRAPHER_INFO_BIN_COUNT; ++bin) {
                  mArrivalBinsLastMinute[bin] -= oldestArrivalBin[bin];
                  mTimestampBinsLastMinute[bin] -= oldestTimestampBin[bin];
              }
          }
          Trace.endSection();
  
          Trace.beginSection("updateInfoGrid");
          for (int bin = 0; bin < CHOREOGRAPHER_INFO_BIN_COUNT; ++bin) {
              updateChoregrapherInfoBin(1, bin, mArrivalBinsLastSecond[bin]);
              updateChoregrapherInfoBin(2, bin, mTimestampBinsLastSecond[bin]);
              updateChoregrapherInfoBin(3, bin, mArrivalBinsLastMinute[bin]);
              updateChoregrapherInfoBin(4, bin, mTimestampBinsLastMinute[bin]);
          }
          Trace.endSection();
  
          Trace.beginSection("updateSwappyStatsGrid");
          for (int stat = 0; stat < 4; ++stat) {
              for (int bin = 0; bin < SWAPPY_STATS_BIN_COUNT; ++bin) {
                  updateSwappyStatsBin(stat +1, bin, nGetSwappyStats(stat, bin));
              }
          }
          TextView appOffsetView = findViewById(R.id.swappy_stats);
          appOffsetView.setText(String.format(Locale.US, "SwappyStats: %d Total Frames", nGetSwappyStats(-1, 0)));
          Trace.endSection();
  
          Trace.beginSection("clearSecondBins");
          for (int bin = 0; bin < CHOREOGRAPHER_INFO_BIN_COUNT; ++bin) {
              mArrivalBinsLastSecond[bin] = 0;
              mTimestampBinsLastSecond[bin] = 0;
          }
          Trace.endSection();
  
          Trace.endSection();
      }
  
      @Override
      public void doFrame(long frameTimeNanos) {
          Trace.beginSection("doFrame");
  
          TextView fpsView = findViewById(R.id.fps);
          fpsView.setText(String.format(Locale.US, "FPS: %.1f", nGetAverageFps()));
  
          long now = System.nanoTime();
  
          if (mIsRunning) {
              Trace.beginSection("Requesting callback");
              Choreographer.getInstance().postFrameCallback(this);
              Trace.endSection();
          }
  
          long arrivalDelta = now - mLastArrivalTime;
          long timestampDelta = now - mLastFrameTimestamp;
          mLastArrivalTime = now;
          mLastFrameTimestamp = frameTimeNanos;
  
          mArrivalBinsLastSecond[Math.min(deltaToBin(arrivalDelta), mArrivalBinsLastSecond.length - 1)] += 1;
          mTimestampBinsLastSecond[Math.min(deltaToBin(timestampDelta), mTimestampBinsLastSecond.length - 1)] += 1;
  
          if (now - mLastDumpTime > 1000000000) {
              dumpBins();
              // Trim off excess precision so we don't drift forward over time
              mLastDumpTime = now - (now % 1000000000);
          }
  
          Trace.endSection();
      }
  
      @Override
      public void surfaceCreated(SurfaceHolder holder) {
          // Do nothing here, waiting for surfaceChanged instead
      }
  
      @Override
      public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
          Surface surface = holder.getSurface();
          nSetSurface(surface, width, height);
      }
  
      @Override
      public void surfaceDestroyed(SurfaceHolder holder) {
          nClearSurface();
      }
  
      public native void nInit();
      public native void nSetSurface(Surface surface, int width, int height);
      public native void nClearSurface();
      public native void nStart();
      public native void nStop();
      public native void nSetPreference(String key, String value);
      public native void nSetWorkload(int load);
      public native void nSetAutoSwapInterval(boolean enabled);
      public native float nGetAverageFps();
      public native int nGetSwappyStats(int stat, int bin);
  
      private MenuItem mInfoOverlayButton;
  
      private LinearLayout mInfoOverlay;
      private final TextView[][] mChoreographerInfoGrid = new TextView[5][7];
      private final int[][] mLastChoreographerInfoValues = new int[4][6];
  
      private final TextView[][] mSwappyGrid = new TextView[5][7];
      private final int[][] mLastSwappyStatsValues = new int[4][6];
  
      private boolean mIsRunning;
      private boolean mInfoOverlayEnabled = false;
  
      private final ExecutorService mChoreographerExecutor = Executors.newSingleThreadExecutor();
      private final Handler mUIThreadHandler = new Handler(Looper.getMainLooper());
  
      private static final int CHOREOGRAPHER_INFO_BIN_COUNT = 6;
      private static final int SWAPPY_STATS_BIN_COUNT = 6;
      private long mLastDumpTime;
      private long mLastArrivalTime;
      private long mLastFrameTimestamp;
      private final int[] mArrivalBinsLastSecond = new int[CHOREOGRAPHER_INFO_BIN_COUNT];
      private final int[] mArrivalBinsLastMinute = new int[CHOREOGRAPHER_INFO_BIN_COUNT];
      private final Queue<int[]> mArrivalBinQueue = new ArrayDeque<>(60);
      private final int[] mTimestampBinsLastSecond = new int[CHOREOGRAPHER_INFO_BIN_COUNT];
      private final int[] mTimestampBinsLastMinute = new int[CHOREOGRAPHER_INFO_BIN_COUNT];
      private final Queue<int[]> mTimestampBinQueue = new ArrayDeque<>(60);
  }
  