content: /*
   * Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
   *
   * This program is free software; you can redistribute it and/or
   * modify it under the terms of the GNU General Public License
   * as published by the Free Software Foundation; either version 2
   * of the License, or (at your option) any later version.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with this program; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
   * MA  02110-1301, USA.
   */
  package com.achep.acdisplay.ui.activities;
  
  import android.app.Activity;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.SharedPreferences;
  import android.content.pm.ActivityInfo;
  import android.content.pm.ApplicationInfo;
  import android.content.pm.PackageManager;
  import android.content.pm.PackageManager.NameNotFoundException;
  import android.content.res.Resources;
  import android.graphics.drawable.Drawable;
  import android.os.AsyncTask;
  import android.os.Bundle;
  import android.os.SystemClock;
  import android.preference.Preference;
  import android.preference.PreferenceActivity;
  import android.preference.PreferenceFragment;
  import android.text.TextUtils;
  import android.util.Log;
  import android.view.LayoutInflater;
  import android.view.Menu;
  import android.view.MenuItem;
  import android.view.View;
  import android.view.View.OnClickListener;
  import android.view.ViewGroup;
  import android.widget.ArrayAdapter;
  import android.widget.ImageView;
  import android.widget.ListAdapter;
  import android.widget.TextView;
  
  import com.achep.acdisplay.blacklist.AppConfig;
  import com.achep.acdisplay.blacklist.Blacklist;
  import com.achep.acdisplay.ui.fragments.BlacklistAppFragment;
  import com.achep.base.utils.MathUtils;
  import com.achep.headsup.R;
  
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Locale;
  import java.util.concurrent.ConcurrentHashMap;
  
  /**
   * Top-level settings activity to handle single pane and double pane UI layout.
   */
  public class BlacklistActivity extends PreferenceActivity {
  
      private static final String TAG = "BlacklistActivity";
  
      // meta data
      private static final String META_DATA_KEY_HEADER_ID =
              "com.achep.acdisplay.blacklist.TOP_LEVEL_HEADER_ID";
      private static final String META_DATA_KEY_FRAGMENT_CLASS =
              "com.achep.acdisplay.blacklist.FRAGMENT_CLASS";
      private static final String META_DATA_KEY_PARENT_TITLE =
              "com.achep.acdisplay.blacklist.PARENT_FRAGMENT_TITLE";
      private static final String META_DATA_KEY_PARENT_FRAGMENT_CLASS =
              "com.achep.acdisplay.blacklist.PARENT_FRAGMENT_CLASS";
  
      // save state
      private static final String SAVE_KEY_CURRENT_HEADER =
              "com.achep.acdisplay.blacklist.CURRENT_HEADER";
      private static final String SAVE_KEY_PARENT_HEADER =
              "com.achep.acdisplay.blacklist.PARENT_HEADER";
  
      // preferences
      private static final String PREF_KEY_SHOW_SYSTEM_APPS = "show_system_apps";
  
      private String mFragmentClass;
      private int mTopLevelHeaderId;
      private Header mFirstHeader;
      private Header mCurrentHeader;
      private Header mParentHeader;
      private boolean mInLocalHeaderSwitch;
  
      protected HashMap<Integer, Integer> mHeaderIndexMap = new HashMap<>();
  
      private SharedPreferences mPreferences;
  
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          mPreferences = getSharedPreferences(Blacklist.PREF_NAME, Activity.MODE_PRIVATE);
  
          getMetaData();
          mInLocalHeaderSwitch = true;
          super.onCreate(savedInstanceState);
          mInLocalHeaderSwitch = false;
  
          if (!onIsHidingHeaders() && onIsMultiPane()) {
              highlightHeader(mTopLevelHeaderId);
              // Force the title so that it doesn't get overridden by a direct launch of
              // a specific settings screen.
              setTitle(R.string.settings);
          }
  
          // Retrieve any saved state
          if (savedInstanceState != null) {
              mCurrentHeader = savedInstanceState.getParcelable(SAVE_KEY_CURRENT_HEADER);
              mParentHeader = savedInstanceState.getParcelable(SAVE_KEY_PARENT_HEADER);
          }
  
          // If the current header was saved, switch to it
          if (savedInstanceState != null && mCurrentHeader != null) {
              //switchToHeaderLocal(mCurrentHeader);
              showBreadCrumbs(mCurrentHeader.title, null);
          }
  
          if (mParentHeader != null) {
              setParentTitle(mParentHeader.title, null, new OnClickListener() {
                  @Override
                  public void onClick(View v) {
                      switchToParent(mParentHeader.fragment);
                  }
              });
          }
  
          // Override up navigation for multi-pane, since we handle it in the fragment breadcrumbs
          if (onIsMultiPane()) {
              getActionBar().setDisplayHomeAsUpEnabled(false);
              getActionBar().setHomeButtonEnabled(false);
          }
      }
  
      @Override
      protected void onSaveInstanceState(Bundle outState) {
          super.onSaveInstanceState(outState);
  
          // Save the current fragment, if it is the same as originally launched
          if (mCurrentHeader != null) {
              outState.putParcelable(SAVE_KEY_CURRENT_HEADER, mCurrentHeader);
          }
          if (mParentHeader != null) {
              outState.putParcelable(SAVE_KEY_PARENT_HEADER, mParentHeader);
          }
      }
  
      @Override
      public void onResume() {
          super.onResume();
          invalidateHeaders();
      }
  
      @Override
      public boolean onCreateOptionsMenu(Menu menu) {
          getMenuInflater().inflate(R.menu.blacklist, menu);
          menu.findItem(R.id.show_system_apps).setChecked(shouldShowSystemApps());
          return true;
      }
  
      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
          switch (item.getItemId()) {
              case R.id.show_system_apps:
                  item.setChecked(!item.isChecked());
                  mPreferences
                          .edit()
                          .putBoolean(PREF_KEY_SHOW_SYSTEM_APPS, item.isChecked())
                          .apply();
                  invalidateHeaders();
                  break;
              default:
                  return super.onOptionsItemSelected(item);
          }
          return true;
      }
  
      private boolean shouldShowSystemApps() {
          return mPreferences.getBoolean(PREF_KEY_SHOW_SYSTEM_APPS, false);
      }
  
      private void updateIcons() {
          ListAdapter listAdapter = getListAdapter();
          if (listAdapter instanceof HeaderAdapter) {
              ((HeaderAdapter) listAdapter).loadIcons();
          }
      }
  
      /**
       * Fills header list with a list of installed apps.
       */
      private void buildHeaderList(List<Header> headers, boolean showSystemApps) {
          String fragmentName = BlacklistAppFragment.class.getCanonicalName();
          int id = 1;
  
          PackageManager pm = getPackageManager();
          List<ApplicationInfo> packages = pm.getInstalledApplications(PackageManager.GET_META_DATA);
          for (ApplicationInfo app : packages) {
              int systemFlag = ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
              int system = MathUtils.bool((app.flags & systemFlag) != 0);
  
              if (system == 1 && !showSystemApps) {
                  continue;
              }
  
              Header header = new Header();
              header.fragment = fragmentName;
              header.title = app.loadLabel(pm); // TODO: This is a huge operation
              header.summary = app.packageName;
              header.id = system | id++ << 1; // Store if system app to id
  
              // Add package name for the fragment
              Bundle args = new Bundle();
              args.putString(BlacklistAppFragment.ARGS_PACKAGE_NAME, app.packageName);
              header.fragmentArguments = args;
  
              headers.add(header);
          }
  
          // Sort by app name
          Collections.sort(headers, new Comparator<Header>() {
              @Override
              public int compare(Header header1, Header header2) {
                  String title1 = header1.title.toString();
                  String title2 = header2.title.toString();
                  return title1.compareToIgnoreCase(title2);
              }
          });
      }
  
      @Override
      protected boolean isValidFragment(String fragmentName) {
          return true;
      }
  
      private void switchToHeaderLocal(Header header) {
          mInLocalHeaderSwitch = true;
          switchToHeader(header);
          mInLocalHeaderSwitch = false;
      }
  
      @Override
      public void switchToHeader(Header header) {
          if (!mInLocalHeaderSwitch) {
              mCurrentHeader = null;
              mParentHeader = null;
          }
          super.switchToHeader(header);
      }
  
      /**
       * Switch to parent fragment and store the grand parent's info
       *
       * @param className name of the activity wrapper for the parent fragment.
       */
      private void switchToParent(String className) {
          final ComponentName cn = new ComponentName(this, className);
          try {
              final PackageManager pm = getPackageManager();
              final ActivityInfo parentInfo = pm.getActivityInfo(cn, PackageManager.GET_META_DATA);
  
              if (parentInfo != null && parentInfo.metaData != null) {
                  String fragmentClass = parentInfo.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
                  CharSequence fragmentTitle = parentInfo.loadLabel(pm);
                  Header parentHeader = new Header();
                  parentHeader.fragment = fragmentClass;
                  parentHeader.title = fragmentTitle;
                  mCurrentHeader = parentHeader;
  
                  switchToHeaderLocal(parentHeader);
                  highlightHeader(mTopLevelHeaderId);
  
                  mParentHeader = new Header();
                  mParentHeader.fragment
                          = parentInfo.metaData.getString(META_DATA_KEY_PARENT_FRAGMENT_CLASS);
                  mParentHeader.title = parentInfo.metaData.getString(META_DATA_KEY_PARENT_TITLE);
              }
          } catch (NameNotFoundException nnfe) {
              Log.w(TAG, "Could not find parent activity : " + className);
          }
      }
  
      @Override
      public void onNewIntent(Intent intent) {
          super.onNewIntent(intent);
  
          // If it is not launched from history, then reset to top-level
          if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
              if (mFirstHeader != null && !onIsHidingHeaders() && onIsMultiPane()) {
                  switchToHeaderLocal(mFirstHeader);
              }
              getListView().setSelectionFromTop(0, 0);
          }
      }
  
      private void highlightHeader(int id) {
          if (id != 0) {
              Integer index = mHeaderIndexMap.get(id);
              if (index != null) {
                  getListView().setItemChecked(index, true);
                  if (isMultiPane()) {
                      getListView().smoothScrollToPosition(index);
                  }
              }
          }
      }
  
      @Override
      public Intent getIntent() {
          Intent superIntent = super.getIntent();
          String startingFragment = getStartingFragmentClass(superIntent);
          // This is called from super.onCreate, isMultiPane() is not yet reliable
          // Do not use onIsHidingHeaders either, which relies itself on this method
          if (startingFragment != null && !onIsMultiPane()) {
              Intent modIntent = new Intent(superIntent);
              modIntent.putExtra(EXTRA_SHOW_FRAGMENT, startingFragment);
              Bundle args = superIntent.getExtras();
              if (args != null) {
                  args = new Bundle(args);
              } else {
                  args = new Bundle();
              }
              args.putParcelable("intent", superIntent);
              modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, superIntent.getExtras());
              return modIntent;
          }
          return superIntent;
      }
  
      /**
       * Checks if the component name in the intent is different from the Settings class and
       * returns the class name to load as a fragment.
       */
      protected String getStartingFragmentClass(Intent intent) {
          if (mFragmentClass != null) return mFragmentClass;
          String intentClass = intent.getComponent().getClassName();
          if (intentClass.equals(getClass().getName())) return null;
  
          return intentClass;
      }
  
      /**
       * Override initial header when an activity-alias is causing Settings to be launched
       * for a specific fragment encoded in the android:name parameter.
       */
      @Override
      public Header onGetInitialHeader() {
          String fragmentClass = getStartingFragmentClass(super.getIntent());
          if (fragmentClass != null) {
              Header header = new Header();
              header.fragment = fragmentClass;
              header.title = getTitle();
              header.fragmentArguments = getIntent().getExtras();
              mCurrentHeader = header;
              return header;
          }
  
          return mFirstHeader;
      }
  
      @Override
      public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args,
                                               int titleRes, int shortTitleRes) {
          return super
                  .onBuildStartFragmentIntent(fragmentName, args, titleRes, shortTitleRes)
                  .setClass(this, SubBlacklistActivity.class);
      }
  
      /**
       * Populate the activity with the top-level headers.
       */
      @Override
      public void onBuildHeaders(List<Header> headers) {
          if (!onIsHidingHeaders()) {
              buildHeaderList(headers, shouldShowSystemApps());
              updateHeaderList(headers);
              updateIcons();
          }
      }
  
      private void updateHeaderList(List<Header> target) {
          int i = 0;
          mHeaderIndexMap.clear();
          while (i < target.size()) {
              Header header = target.get(i);
              // Ids are integers, so downcasting
              int id = (int) header.id;
  
              // Increment if the current one wasn't removed by the Utils code.
              if (i < target.size() && target.get(i) == header) {
                  // Hold on to the first header, when we need to reset to the top-level
                  int headerType = HeaderAdapter.getHeaderType(header);
                  if (mFirstHeader == null &&
                          headerType != HeaderAdapter.HEADER_TYPE_CATEGORY) {
                      mFirstHeader = header;
                  }
                  mHeaderIndexMap.put(id, i);
                  i++;
              }
          }
      }
  
      private void getMetaData() {
          try {
              ActivityInfo ai = getPackageManager().getActivityInfo(getComponentName(),
                      PackageManager.GET_META_DATA);
              if (ai == null || ai.metaData == null) return;
              mTopLevelHeaderId = ai.metaData.getInt(META_DATA_KEY_HEADER_ID);
              mFragmentClass = ai.metaData.getString(META_DATA_KEY_FRAGMENT_CLASS);
  
              // Check if it has a parent specified and create a Header object
              final int parentHeaderTitleRes = ai.metaData.getInt(META_DATA_KEY_PARENT_TITLE);
              String parentFragmentClass = ai.metaData.getString(META_DATA_KEY_PARENT_FRAGMENT_CLASS);
              if (parentFragmentClass != null) {
                  mParentHeader = new Header();
                  mParentHeader.fragment = parentFragmentClass;
                  if (parentHeaderTitleRes != 0) {
                      mParentHeader.title = getResources().getString(parentHeaderTitleRes);
                  }
              }
          } catch (NameNotFoundException e) {
              // No recovery
          }
      }
  
      private static class HeaderAdapter extends ArrayAdapter<Header> {
  
          static final int HEADER_TYPE_CATEGORY = 0;
          static final int HEADER_TYPE_NORMAL = 1;
  
          private static final int HEADER_TYPE_COUNT = 2;
  
          private final Context mContext;
          private final LayoutInflater mInflater;
  
          private final PackageManager mPackageManager;
          private final Drawable mDefaultImg;
          private final List<Header> mHeaders;
          private LoadIconsTask mLoadIconsTask;
  
          private final ConcurrentHashMap<String, Drawable> mIcons;
  
          static int getHeaderType(Header header) {
              if (header.fragment == null && header.intent == null) {
                  return HEADER_TYPE_CATEGORY;
              } else {
                  return HEADER_TYPE_NORMAL;
              }
          }
  
          @Override
          public int getItemViewType(int position) {
              Header header = getItem(position);
              return getHeaderType(header);
          }
  
          @Override
          public boolean areAllItemsEnabled() {
              return false; // because of categories
          }
  
          @Override
          public boolean isEnabled(int position) {
              return getItemViewType(position) != HEADER_TYPE_CATEGORY;
          }
  
          @Override
          public int getViewTypeCount() {
              return HEADER_TYPE_COUNT;
          }
  
          @Override
          public boolean hasStableIds() {
              return true;
          }
  
          public HeaderAdapter(Context context, List<Header> headers) {
              super(context, 0, headers);
  
              mContext = context;
              mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
              mPackageManager = context.getPackageManager();
              mHeaders = headers;
  
              // Set the default icon till the actual app icon is loaded in async task
              //noinspection ResourceType
              mDefaultImg = context.getResources().getDrawable(android.R.mipmap.sym_def_app_icon);
  
              mIcons = new ConcurrentHashMap<>();
  
              loadIcons();
          }
  
          private static class Holder {
              ImageView icon;
              TextView title;
              TextView summary;
          }
  
          @Override
          public View getView(int position, View convertView, ViewGroup parent) {
              final Holder holder;
              final Header header = getItem(position);
              final int headerType = getHeaderType(header);
              View view = null;
  
              if (convertView == null) {
                  holder = new Holder();
                  switch (headerType) {
                      case HEADER_TYPE_CATEGORY:
                          view = new TextView(getContext(), null,
                                  android.R.attr.listSeparatorTextViewStyle);
                          holder.title = (TextView) view;
                          break;
  
                      case HEADER_TYPE_NORMAL:
                          view = mInflater.inflate(
                                  R.layout.preference_header_item, parent,
                                  false);
                          holder.icon = (ImageView) view.findViewById(R.id.icon);
                          holder.title = (TextView) view.findViewById(android.R.id.title);
                          holder.summary = (TextView) view.findViewById(android.R.id.summary);
                          break;
                  }
                  view.setTag(holder);
              } else {
                  view = convertView;
                  holder = (Holder) view.getTag();
              }
  
              Resources res = mContext.getResources();
  
              // All view fields must be updated every time, because the view may be recycled
              switch (headerType) {
                  case HEADER_TYPE_CATEGORY:
                      holder.title.setText(header.getTitle(getContext().getResources()));
                      break;
  
                  case HEADER_TYPE_NORMAL:
                      String packageName = "" + header.summary;
                      AppConfig config = Blacklist.getInstance().getAppConfig(packageName);
  
                      // Show checked options in summary.
                      // TODO: Find the way to optimize it.
                      if (config.isHidden() || config.isNonClearableEnabled()) {
                          StringBuilder sb = new StringBuilder();
                          boolean empty = true;
  
                          int[] pairs = new int[]{
                                  // Hidden
                                  MathUtils.bool(config.isHidden()),
                                  R.string.blacklist_app_hide_title,
                                  // Non-clearable
                                  MathUtils.bool(config.isNonClearableEnabled()),
                                  R.string.blacklist_app_non_clearable_title,
                          };
  
                          // Append checked options.
                          String divider = res.getString(R.string.settings_multi_list_divider);
                          for (int i = 0; i < pairs.length / 2; i++) {
                              int a = pairs[i * 2];
                              if (a == 1) {
                                  if (!empty) {
                                      sb.append(divider);
                                  }
                                  sb.append(res.getString(pairs[i * 2 + 1]));
                                  empty = false;
                              }
                          }
  
                          String summary = sb.toString();
                          if (!TextUtils.isEmpty(summary)) {
                              // Keep only first letter with upper case and
                              // force all other to lower case.
                              summary = summary.charAt(0)
                                      + summary.substring(1).toLowerCase(Locale.getDefault());
  
                              holder.summary.setVisibility(View.VISIBLE);
                              holder.summary.setText(summary);
                          } else {
                              holder.summary.setVisibility(View.GONE);
                          }
                      } else {
                          holder.summary.setVisibility(View.GONE);
                      }
  
                      Drawable icon = mIcons.get(packageName);
                      holder.icon.setImageDrawable(icon != null ? icon : mDefaultImg);
                      holder.title.setText(header.title != null ? header.title : packageName);
                      break;
              }
  
              return view;
          }
  
          /**
           * Update the list of apps' icons
           */
          public void loadIcons() {
              if (mLoadIconsTask != null && !mLoadIconsTask.getStatus()
                      .equals(AsyncTask.Status.FINISHED)) {
                  mLoadIconsTask.cancel = true;
                  mLoadIconsTask.cancel(false);
              }
              mLoadIconsTask = new LoadIconsTask();
              mLoadIconsTask.execute(mHeaders.toArray(new Header[mHeaders.size()]));
          }
  
          /**
           * An asynchronous task to load the icons & titles of the installed applications.
           */
          // TODO: Maybe use SoftReference<> to save some memory.
          private class LoadIconsTask extends AsyncTask<Header, Void, Void> {
  
              private volatile long time;
              private volatile boolean cancel;
  
              @Override
              protected Void doInBackground(Header... headers) {
                  for (Header header : headers) {
                      try {
                          if (cancel) return null;
  
                          String packageName = "" + header.summary;
                          if (mIcons.containsKey(packageName)) {
                              continue;
                          }
  
                          ApplicationInfo app = mPackageManager.getApplicationInfo(packageName, 0);
                          Drawable icon = mPackageManager.getApplicationIcon(app.packageName);
                          mIcons.put(app.packageName, icon);
  
                          long now = SystemClock.uptimeMillis();
                          if (now - time > 500) {
                              publishProgress();
                              time = now;
                          }
                      } catch (NameNotFoundException e) {
                          // ignored; app will show up with default image & title
                      }
                  }
                  return null;
              }
  
              @Override
              protected void onProgressUpdate(Void... progress) {
                  notifyDataSetChanged();
              }
  
              @Override
              protected void onPostExecute(Void aVoid) {
                  super.onPostExecute(aVoid);
                  notifyDataSetChanged();
              }
          }
      }
  
      @Override
      public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) {
          startPreferencePanel(
                  pref.getFragment(),
                  pref.getExtras(),
                  pref.getTitleRes(),
                  pref.getTitle(), null, 0);
          return true;
      }
  
      @Override
      public boolean shouldUpRecreateTask(Intent targetIntent) {
          return super.shouldUpRecreateTask(new Intent(this, BlacklistActivity.class));
      }
  
      @Override
      public void setListAdapter(ListAdapter adapter) {
          if (adapter == null) {
              super.setListAdapter(null);
          } else {
              List<Header> headers = null;
              try {
                  Method method = PreferenceActivity.class.getDeclaredMethod("getHeaders");
                  method.setAccessible(true);
                  headers = (List<Header>) method.invoke(this);
              } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
                  e.printStackTrace();
              }
  
              super.setListAdapter(new HeaderAdapter(this, headers));
          }
      }
  }
  