content: package com.mxt.anitrend.base.custom.activity;
  
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.os.Build;
  import android.os.Bundle;
  import android.speech.RecognizerIntent;
  import android.text.TextUtils;
  import android.view.Menu;
  import android.view.MenuItem;
  import android.view.View;
  import android.view.Window;
  import android.widget.TextView;
  import android.widget.Toast;
  import android.widget.Toolbar;
  
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.appcompat.app.ActionBar;
  import androidx.appcompat.app.AppCompatActivity;
  import androidx.core.app.ActivityCompat;
  import androidx.core.content.ContextCompat;
  import androidx.core.view.ViewCompat;
  import androidx.core.view.WindowCompat;
  import androidx.core.view.WindowInsetsCompat;
  import androidx.lifecycle.Observer;
  import androidx.lifecycle.ViewModelProvider;
  
  import com.miguelcatalan.materialsearchview.MaterialSearchView;
  import com.mxt.anitrend.R;
  import com.mxt.anitrend.base.custom.fragment.FragmentBase;
  import com.mxt.anitrend.base.custom.presenter.CommonPresenter;
  import com.mxt.anitrend.base.custom.sheet.BottomSheetBase;
  import com.mxt.anitrend.base.custom.viewmodel.ViewModelBase;
  import com.mxt.anitrend.base.interfaces.event.ResponseCallback;
  import com.mxt.anitrend.databinding.CustomToolbarBinding;
  import com.mxt.anitrend.extension.KoinExt;
  import com.mxt.anitrend.util.CompatUtil;
  import com.mxt.anitrend.util.ConfigurationUtil;
  import com.mxt.anitrend.util.IntentBundleUtil;
  import com.mxt.anitrend.util.KeyUtil;
  import com.mxt.anitrend.util.NotifyUtil;
  import com.mxt.anitrend.util.Settings;
  import com.mxt.anitrend.util.media.MediaActionUtil;
  import com.mxt.anitrend.view.activity.index.MainActivity;
  import com.mxt.anitrend.view.activity.index.SearchActivity;
  
  import org.greenrobot.eventbus.EventBus;
  
  import java.util.ArrayList;
  import java.util.Locale;
  import java.util.Objects;
  
  import butterknife.BindView;
  import timber.log.Timber;
  
  
  /**
   * Created by max on 2017/06/09.
   * Activity base <M type of data model, P extends CommonPresenter>
   */
  public abstract class ActivityBase<M, P extends CommonPresenter> extends AppCompatActivity implements
          Observer<M>, CommonPresenter.AbstractPresenter<P>, ResponseCallback,
          MaterialSearchView.SearchViewListener, MaterialSearchView.OnQueryTextListener {
  
      private static final String KEY_SEARCHVIEW_QUERY = "SEARCHVIEW_QUERY";
  
      protected String TAG;
  
      protected @Nullable @BindView(R.id.search_view) MaterialSearchView mSearchView;
  
      private ViewModelBase<M> viewModel;
  
      protected BottomSheetBase mBottomSheet;
      protected FragmentBase mFragment;
      protected ActionBar mActionBar;
      protected IntentBundleUtil intentBundleUtil;
      protected MediaActionUtil mediaActionUtil;
  
      protected long id;
  
      protected int offScreenLimit = 3;
      protected boolean disableNavigationStyle;
      protected static final int REQUEST_PERMISSION = 102;
  
      private boolean isClosing;
  
      private CommonPresenter presenter;
      @Nullable
      protected ConfigurationUtil configurationUtil;
  
      /**
       * Some activities may have custom themes and if that's the case
       * override this method and set your own theme style.
       *
       * @see ConfigurationUtil
       */
      protected void configureActivity() {
          if (configurationUtil == null)
              configurationUtil = KoinExt.get(ConfigurationUtil.class);
          configurationUtil.onCreateAttach(this);
      }
  
      @Override
      protected void onCreate(@Nullable Bundle savedInstanceState) {
          TAG = getClass().getSimpleName();
          configureActivity();
          enableEdgeToEdge();
          super.onCreate(savedInstanceState);
          intentBundleUtil = new IntentBundleUtil(getIntent());
          intentBundleUtil.checkIntentData(this);
      }
  
      /**
       * Enables edge-to-edge display and handles system window insets properly
       * for content that should extend behind system bars.
       * This method should be called before super.onCreate() to ensure proper setup.
       */
      protected void enableEdgeToEdge() {
          // Enable edge-to-edge display
          WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
          
          // For light themes, ensure status bar icons are dark
          if (configurationUtil != null) {
              Settings settings = KoinExt.get(Settings.class);
              if (CompatUtil.INSTANCE.isLightTheme(settings)) {
                  WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView())
                      .setAppearanceLightStatusBars(true);
                  WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView())
                      .setAppearanceLightNavigationBars(true);
              }
          }
      }
  
      @Override
      protected void onPostCreate(@Nullable Bundle savedInstanceState) {
          super.onPostCreate(savedInstanceState);
          if(mSearchView != null) {
              mSearchView.setVoiceSearch(true);
              mSearchView.setOnQueryTextListener(this);
              mSearchView.setOnSearchViewListener(this);
              mSearchView.setCursorDrawable(R.drawable.material_search_cursor);
          }
      }
  
      @Override
      protected void onSaveInstanceState(@NonNull Bundle outState) {
          super.onSaveInstanceState(outState);
  
          if (mSearchView != null) {
              CharSequence text = ((TextView) mSearchView.findViewById(R.id.searchTextView)).getText();
              if (!TextUtils.isEmpty(text))
                  outState.putCharSequence(KEY_SEARCHVIEW_QUERY, text);
          }
      }
  
      @Override
      protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
          super.onRestoreInstanceState(savedInstanceState);
  
          if (savedInstanceState.containsKey(KEY_SEARCHVIEW_QUERY))
              onQueryTextChange(savedInstanceState.getCharSequence(KEY_SEARCHVIEW_QUERY).toString());
      }
  
      /**
       * Set a {@link Toolbar Toolbar} to act as the
       * {@link ActionBar} for this Activity window.
       * <p>
       * <p>When set to a non-null value the {@link #getActionBar()} method will return
       * an {@link ActionBar} object that can be used to control the given
       * toolbar as if it were a traditional window decor action bar. The toolbar's menu will be
       * populated with the Activity's options menu and the main_navigation button will be wired through
       * the standard {@link android.R.id#home home} menu select action.</p>
       * <p>
       * <p>In order to use a Toolbar within the Activity's window content the application
       * must not request the window feature
       * {@link Window#FEATURE_ACTION_BAR FEATURE_SUPPORT_ACTION_BAR}.</p>
       *
       * @param toolbar Toolbar to set as the Activity's action bar, or {@code null} to clear it
       */
      @Override
      public void setSupportActionBar(@Nullable androidx.appcompat.widget.Toolbar toolbar) {
          super.setSupportActionBar(toolbar);
          setHomeUp();
      }
  
      private void setHomeUp() {
          if((mActionBar = getSupportActionBar()) != null)
              mActionBar.setDisplayHomeAsUpEnabled(true);
      }
  
      public void disableToolbarTitle() {
          final ActionBar actionBar = getSupportActionBar();
          if (actionBar != null) {
              actionBar.setDisplayShowTitleEnabled(false);
          }
      }
  
      protected void setTransparentStatusBar() {
          Window window = getWindow();
          window.getDecorView().setSystemUiVisibility(
                  View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                          | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
          int color = ContextCompat.getColor(this, android.R.color.transparent);
          window.setStatusBarColor(color);
      }
  
      protected void setTransparentStatusBarWithColor() {
          Window window = getWindow();
          int color = ContextCompat.getColor(this, R.color.colorTransparent);
          window.setStatusBarColor(color);
          window.setNavigationBarColor(color);
      }
  
      /**
       * Helper method to setup window insets for edge-to-edge content.
       * This should be called on the root view of your layout after setContentView().
       * 
       * @param rootView The root view of the activity's layout
       */
      protected void setupEdgeToEdgeContent(View rootView) {
          ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, windowInsets) -> {
              int systemBarsType = WindowInsetsCompat.Type.systemBars();
              
              // Get system bar insets
              int statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
              int navigationBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
              int leftInset = windowInsets.getInsets(systemBarsType).left;
              int rightInset = windowInsets.getInsets(systemBarsType).right;
              
              // Apply padding to the root view to avoid content overlap
              v.setPadding(leftInset, statusBarHeight, rightInset, navigationBarHeight);
              
              // Return the insets unchanged for child views to handle if needed
              return windowInsets;
          });
      }
  
      /**
       * Helper method to setup window insets for toolbars when using edge-to-edge.
       * This ensures the toolbar has proper top padding for the status bar.
       * 
       * @param toolbar The toolbar view to apply insets to
       */
      protected void setupToolbarInsets(androidx.appcompat.widget.Toolbar toolbar) {
          ViewCompat.setOnApplyWindowInsetsListener(toolbar, (v, windowInsets) -> {
              // Get status bar height
              int statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
              
              // Apply top padding to toolbar for status bar
              int currentPaddingLeft = v.getPaddingLeft();
              int currentPaddingRight = v.getPaddingRight();
              int currentPaddingBottom = v.getPaddingBottom();
              
              v.setPadding(currentPaddingLeft, statusBarHeight, currentPaddingRight, currentPaddingBottom);
              
              return windowInsets;
          });
      }
  
      @Override
      public boolean onCreateOptionsMenu(Menu menu) {
          return super.onCreateOptionsMenu(menu);
      }
  
      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
          if (item.getItemId() == android.R.id.home)
              onBackPressed();
          return super.onOptionsItemSelected(item);
      }
  
      protected boolean requestPermissionIfMissing(String permission) {
          if (ContextCompat.checkSelfPermission(this,
                  permission) == PackageManager.PERMISSION_GRANTED) {
              return true;
          }
          else if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permission))
              ActivityCompat.requestPermissions(this, new String[]{permission}, REQUEST_PERMISSION);
          return false;
      }
  
      /**
       * Check to see if activity is still alive
       * <br/>
       *
       * @return true if the activity is still valid otherwise false
       */
      protected boolean isAlive() {
          return !isFinishing() || !isDestroyed();
      }
  
      /**
       * Callback for the result from requesting permissions. This method
       * is invoked for every call on {@link #requestPermissions(String[], int)}.
       * <p>
       * <strong>Note:</strong> It is possible that the permissions request interaction
       * with the user is interrupted. In this case you will receive empty permissions
       * and results arrays which should be treated as a cancellation.
       * </p>
       *
       * @param requestCode  The request code passed in {@link #requestPermissions(String[], int)}.
       * @param permissions  The requested permissions. Never null.
       * @param grantResults The grant results for the corresponding permissions
       *                     which is either {@link PackageManager#PERMISSION_GRANTED}
       *                     or {@link PackageManager#PERMISSION_DENIED}. Never null.
       * @see #requestPermissions(String[], int)
       */
      @Override
      public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
          super.onRequestPermissionsResult(requestCode, permissions, grantResults);
          if (requestCode == REQUEST_PERMISSION) {
              for (int i = 0; i < permissions.length; i++) {
                  if (grantResults[i] == PackageManager.PERMISSION_GRANTED)
                      onPermissionGranted(permissions[i]);
                  else
                      NotifyUtil.INSTANCE.makeText(this, R.string.text_permission_required, R.drawable.ic_warning_white_18dp, Toast.LENGTH_SHORT).show();
              }
          }
      }
  
      /**
       * Called for each of the requested permissions as they are granted
       *
       * @param permission the current permission granted
       */
      protected void onPermissionGranted(@NonNull String permission) {
          Timber.tag(TAG).d("Granted %s", permission);
      }
  
      /**
       * Dispatch onPause() to fragments.
       */
      @Override
      protected void onPause() {
          super.onPause();
          if(EventBus.getDefault().isRegistered(this))
              EventBus.getDefault().unregister(this);
          if(mediaActionUtil != null)
              mediaActionUtil.onPause(null);
          if(presenter != null)
              presenter.onPause(null);
      }
  
      /**
       * Dispatch onResume() to fragments.  Note that for better inter-operation
       * with older versions of the platform, at the point of this call the
       * fragments attached to the activity are <em>not</em> resumed.  This means
       * that in some cases the previous state may still be saved, not allowing
       * fragment transactions that modify the state.  To correctly interact
       * with fragments in their proper state, you should instead override
       * {@link #onResumeFragments()}.
       */
      @Override
      protected void onResume() {
          super.onResume();
          if (configurationUtil != null)
              configurationUtil.onResumeAttach(this);
          if(mediaActionUtil != null)
              mediaActionUtil.onResume(null);
          if(presenter != null)
              presenter.onResume(null);
      }
  
      @Override
      protected void onDestroy() {
          if(mediaActionUtil != null)
              mediaActionUtil.onDestroy();
          if(presenter != null)
              presenter.onDestroy();
          super.onDestroy();
      }
  
      /**
       * Take care of popping the fragment back stack or finishing the activity
       * as appropriate.
       */
      @Override
      public void onBackPressed() {
          if(mFragment != null && mFragment.onBackPress())
              return;
          if(mSearchView != null && mSearchView.isSearchOpen()) {
              mSearchView.closeSearch();
              return;
          } if(this instanceof MainActivity && !isClosing) {
              NotifyUtil.INSTANCE.makeText(this, R.string.text_confirm_exit, R.drawable.ic_home_white_24dp, Toast.LENGTH_SHORT).show();
              isClosing = true;
              return;
          }
          super.onBackPressed();
      }
  
      /**
       * Make decisions, check for permissions or fire background threads from this method
       * N.B. Must be called after onPostCreate
       */
      protected abstract void onActivityReady();
  
      protected abstract void updateUI();
  
      protected abstract void makeRequest();
  
      @Override
      protected void onActivityResult(int requestCode, int resultCode, Intent data) {
          if (requestCode == MaterialSearchView.REQUEST_VOICE && resultCode == RESULT_OK) {
              ArrayList<String> matches = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
              if (matches != null && !matches.isEmpty()) {
                  String searchWrd = matches.getFirst();
                  if (!TextUtils.isEmpty(searchWrd) && mSearchView != null)
                      mSearchView.setQuery(searchWrd, false);
              }
              return;
          }
          super.onActivityResult(requestCode, resultCode, data);
      }
  
      @SuppressWarnings("unchecked")
      @Override
      public P getPresenter() {
          return (P) presenter;
      }
  
      public void setPresenter(CommonPresenter presenter) {
          this.presenter = presenter;
      }
  
      protected ViewModelBase<M> getViewModel() {
          return viewModel;
      }
  
      @SuppressWarnings("unchecked")
      protected void setViewModel(boolean stateSupported) {
          if(viewModel == null) {
              ViewModelProvider provider = new ViewModelProvider(this);
              viewModel = provider.get(ViewModelBase.class);
              viewModel.setContext(this);
              if(!viewModel.getModel().hasActiveObservers())
                  viewModel.getModel().observe(this, this);
              if(stateSupported)
                  viewModel.setState(this);
          }
      }
  
      @Nullable
      protected M getModel() {
          return viewModel != null ? viewModel.snapshot() : null;
      }
  
      /**
       * Called when the model state is changed.
       *
       * @param model The new data
       */
      @Override
      public void onChanged(@Nullable M model) {
          Timber.tag(TAG).v("onChanged() from view model has received data");
      }
  
      @Override
      public void showError(String error) {
          if(!TextUtils.isEmpty(error))
              Timber.tag(TAG).w(error);
          if(isAlive()) {
              NotifyUtil.INSTANCE.createAlerter(this, getString(R.string.text_error_request), error,
                      R.drawable.ic_warning_white_18dp, R.color.colorStateOrange,
                      KeyUtil.DURATION_MEDIUM);
          }
  
      }
  
      @Override
      public void showEmpty(String message) {
          if(!TextUtils.isEmpty(message))
              Timber.tag(TAG).v(message);
          if (isAlive()) {
              NotifyUtil.INSTANCE.createAlerter(this, getString(R.string.text_error_request), message,
                      R.drawable.ic_warning_white_18dp, R.color.colorStateBlue,
                      KeyUtil.DURATION_MEDIUM);
          }
      }
  
      protected void showBottomSheet() {
          mBottomSheet.show(getSupportFragmentManager(), mBottomSheet.getTag());
      }
  
      /**
       * Called when the user submits the query. This could be due to a key press on the
       * keyboard or due to pressing a submit button.
       * The listener can override the standard behavior by returning true
       * to indicate that it has handled the submit request. Otherwise return false to
       * let the SearchView handle the submission by launching any associated intent.
       *
       * @param query the query text that is to be submitted
       * @return true if the query has been handled by the listener, false to let the
       * SearchView perform the default action.
       */
      @Override
      public boolean onQueryTextSubmit(String query) {
          if(!TextUtils.isEmpty(query)) {
              Intent intent = new Intent(this, SearchActivity.class);
              intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
              intent.putExtra(KeyUtil.arg_search, query);
              CompatUtil.INSTANCE.startRevealAnim(this, mSearchView, intent);
              return true;
          }
          NotifyUtil.INSTANCE.makeText(this, R.string.text_search_empty, Toast.LENGTH_SHORT).show();
          return false;
      }
  
      /**
       * Called when the query text is changed by the user.
       *
       * @param newText the new content of the query text field.
       * @return false if the SearchView should perform the default action of showing any
       * suggestions if available, true if the action was handled by the listener.
       */
      @Override
      public boolean onQueryTextChange(String newText) {
          if(presenter != null)
              presenter.notifyAllListeners(newText.toLowerCase(Locale.getDefault()), false);
          return false;
      }
  
      @Override
      public void onSearchViewShown() {
  
      }
  
      @Override
      public void onSearchViewClosed() {
          if(presenter != null)
              presenter.notifyAllListeners("", false);
      }
  }
  