Android Open Source - android-autostarts List Activity






From Project

Back to project page android-autostarts.

License

The source code is released under:

GNU General Public License

If you think the Android project android-autostarts listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.elsdoerfer.android.autostarts;
/*  ww w .j av  a  2s  .c o  m*/
import java.util.ArrayList;

import android.app.DialogFragment;
import android.app.ExpandableListActivity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.content.*;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.text.style.ClickableSpan;
import android.view.*;
import android.view.View.OnClickListener;
import android.widget.ExpandableListView;
import android.widget.SearchView;
import android.widget.TextView;
import android.widget.Toast;

import com.elsdoerfer.android.autostarts.db.ComponentInfo;
import com.elsdoerfer.android.autostarts.db.IntentFilterInfo;

public class ListActivity extends ExpandableListActivity {

  static final Boolean LOGV = false;

  static final String PREFS_NAME = "common";
  static final String PREF_FILTER_SYS_APPS = "filter-sys-apps";
  static final String PREF_FILTER_SHOW_CHANGED = "show-changed-only";
  static final String PREF_FILTER_UNKNOWN = "filter-unknown-events";
  static final String PREF_GROUPING = "grouping";

  private Menu mActionBarMenu;
  private MenuItem mExpandCollapseToggleItem;
  private MenuItem mGroupingModeItem;
  MenuItem mReloadItem;
  private Toast mInfoToast;

  MyExpandableListAdapter mListAdapter;
  ArrayList<IntentFilterInfo> mEvents;
  private DatabaseHelper mDb;
  SharedPreferences mPrefs;
  private Boolean mExpandSuggested = true;
  private LoadTask mLoadTask;

  protected ToggleService mToggleService;

  private ServiceConnection mToggleServiceConnection = new ServiceConnection() {
    public void onServiceConnected(ComponentName className, IBinder service) {
      mToggleService = ((ToggleService.LocalBinder)service).getService();
      mToggleService.setHandler(new ToggleService.ToggleServiceListener() {
        @Override
        public void onActivityChange(ComponentInfo component) {
          if (component == null) {
            // Restore quiet state, no item being processed.
            setDefaultTitle();
            ListActivity.this.setProgressBarIndeterminateVisibility(false);
          }
          else {
            // Display info about item being processed
            ListActivity.this.setTitle(String.format(
                getString(R.string.changing_state), component.getLabel()));
            // Note; We are using indeterminate progress mode here, though a
            // proper bar makes sense as well if the user queues multiple changes.
            // It especially makes sense when restoring a backup state.
            // We have to consider a running 'refresh list' process though.
            ListActivity.this.setProgressBarVisibility(false);
            // It seems impossible to show just one progressbar (determinate
            // or indeterminate). The previous line shows the determinate one
            // as well, and setProgressBarVisibility hides the indeterminate one.
            // Setting the progress to 0 sort of works, but on the new flat-blue
            // Android theme anyway you do see the - subtle - empty progressbar.
            ListActivity.this.setProgress(0);
          }
        }

        @Override
        public void onQueueModified(ComponentInfo component, boolean wasAdded) {
          if (!wasAdded) {
            // The component was removed from the queue, presumably
            // successfully processed. We now need to copy the new
            // state from "component" to our local dataset. This is
            // because the "component" instance the service has is a
            // copy of the one used here in the activity, due to it
            // being serialized in a parcel when given to it via an
            // Intent.
            //
            // It's a bit hacky, but the best alternative I can think
            // of would be moving the complete IntentFilterInfo
            // database into a global scope so the service can work
            // directly with it.
            for (IntentFilterInfo info : mEvents) {
              if (info.componentInfo.equals(component))
                info.componentInfo.currentEnabledState =
                    component.currentEnabledState;
            }
          }

          mListAdapter.notifyDataSetInvalidated();
        }
      });

      // The list adapter require access to the ToggleService to show
      // current processing, but we can't delay initialing the list
      // adapter right away in onCreate, or the list view will lose
      // open/collapsed state. I'm not aware that we can make the
      // service binding block. My solution therefore is making
      // access to mToggleService optional in our list adapter impl,
      // and refresh the list once, when the service is finally connected.
      mListAdapter.notifyDataSetChanged();

      // Get us an initial UI update.
      mToggleService.requestUpdate();
    }
    public void onServiceDisconnected(ComponentName className) {}
  };


  @Override
  public void onCreate(final Bundle saved) {
    super.onCreate(saved);

    requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
    requestWindowFeature(Window.FEATURE_PROGRESS);
    setProgressBarIndeterminateVisibility(false);
    setContentView(R.layout.list);
    setDefaultTitle();

    // Set everything up.
    mPrefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
    mDb = new DatabaseHelper(this);
    // This is just to workaround "Can't upgrade read-only database..."
    // exceptions, when an upgrade is necessary.
    mDb.getWritableDatabase().close();
    mListAdapter = new MyExpandableListAdapter(this);
    setListAdapter(mListAdapter);

    // Restore preferences
    mListAdapter.setFilterSystemApps(
        mPrefs.getBoolean(PREF_FILTER_SYS_APPS, false));
    mListAdapter.setShowChangedOnly(
        mPrefs.getBoolean(PREF_FILTER_SHOW_CHANGED, false));
    mListAdapter.setFilterUnknown(
        mPrefs.getBoolean(PREF_FILTER_UNKNOWN, true));
    mListAdapter.setGrouping(mPrefs.getString(PREF_GROUPING, "action").equals("package")
        ? MyExpandableListAdapter.GROUP_BY_PACKAGE
        : MyExpandableListAdapter.GROUP_BY_ACTION);

    bindService(new Intent(this, ToggleService.class),
        mToggleServiceConnection, Context.BIND_AUTO_CREATE);

    // Init/restore retained and instance data. If we have data
    // retained, we can speed things up significantly by not having
    // to do a load.
    Object retained = getLastNonConfigurationInstance();
    if (retained != null) {
      // Be careful not to copy any objects that reference the
      // activity itself, or we would leak it!
      ListActivity oldActivity = (ListActivity) retained;
      mEvents = oldActivity.mEvents;
      mLoadTask = oldActivity.mLoadTask;
      // Display what we have immediately.
      if (mEvents != null)
        apply();
      // Continue loading in case we're not done yet.
      if (mLoadTask != null)
        mLoadTask.attach(this);
    }
    // Otherwise, we are going to have to init certain data
    // ourselves, and load some from from instance state, if
    // available.
    else {
      if (saved != null) {
        /* here's the place to load values from onSaveInstanceState. We
         * currently no longer do so, but I'd like to keep the logic structure. */
      }
      else { /* here's the place to load some prefs, if necessary */ }

      // Initial load.
      loadAndApply();
    }

    // This depends both on preferences on loading status.
    updateEmptyText();
  }

  private void setDefaultTitle() {
    setTitle(R.string.app_name);
  }

  @Override
  public Object onRetainNonConfigurationInstance() {
    // Retain the full activity; it's just simpler as opposed to
    // copying a bunch of stuff to an array; but we need to be very
    // careful here not to leak it when we restore.
    return this;
  }

  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // Storing the last selected event (needed for dialog persistence)
    // is problematic because saving and loading it as a parcable will
    // break the identify of the ComponentInfo/PackageInfo relations.
    // In other words, the attached ComponentInfo will not be the same
    // object as the one we use to render the list items, so changing
    // the "disabledState" of a mLastSelectedEvent restored in this way
    // will not affect the GUI display. There are different approaches
    // can take to solve this (TODO):
    //   - A complicated scheme where IntentFilterInfo stores the
    //     package and component names as strings, and upon restore
    //     from a bundle will look up the actual ComponentInfo parent
    //     as soon as it becomes available. A kind of delayed-loaded
    //     weak reference.
    //   - Since we're only talking about a single property that is
    //     modified by use (disabled state), this data could be stored
    //     by the Activity as a kind of "overlay", as a list of changes
    //     made by the user. We then would no longer need the
    //     ComponentInfo/PackageInfo relations to have a shared identity.
    //   - The ToggleTool, rather than modifing the ComponentInfo
    //     object, could have the Activity's callback method deal with
    //     the state change. If the component is already loaded, it's
    //     state is changed, otherwise, we can assume it will be loaded
    //     correctly (we can assume? would there be race conditions)?
    //   - We could simply store the list of events globally, bypassing
    //     all these problems.
    // Actually, now with the service, where we do parcel the Component
    // anyway, and update via a callback, this might be a moot problem.
    // We can and should get rid of this TODO.

    // TODO: Note that we do not store the event list. In cases where
    // onRetainNonConfigurationInstance() is not available, we will
    // need to reload all events. It would not be *that* hard to store
    // the events list (of course, this only works if loading is
    // finished): We'd just store a list of PackageInfo parcelables,
    // with all ComponentInfo and IntentFilterInfo children, and upon
    // load would simply have to connect each child with it's parent
    // again. However, in the hope that the new Loader API will allow
    // us to avoid all these problems, we don't bother to implement
    // this now.
    // In case the Loader API should prove to be insufficient, I think
    // we would want to switch to a global state.
    super.onSaveInstanceState(outState);
  }

  @Override
  protected void onPause() {
    super.onPause();
    if (mInfoToast != null)
      mInfoToast.cancel();
  }

  @Override
  protected void onDestroy() {
    if (mDb != null)
      mDb.close();
    // Unattach the activity from a potentially running task.
    if (mLoadTask != null)
      mLoadTask.attach(null);
    if (mToggleService != null) {
      unbindService(mToggleServiceConnection);
      mToggleService = null;
    }
    super.onDestroy();
  }

  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu items for use in the action bar
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.actionbar, menu);
    mActionBarMenu = menu;

    mExpandCollapseToggleItem = menu.findItem(R.id.expand);
    mGroupingModeItem = menu.findItem(R.id.grouping);
    mReloadItem = menu.findItem(R.id.reload);

    SearchView search = (SearchView) menu.findItem(R.id.search).getActionView();
    search.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
      @Override
      public boolean onQueryTextSubmit(String query) {
        return false;
      }
      @Override
      public boolean onQueryTextChange(String query) {
        mListAdapter.setTextFilter(query);
        updateEmptyText();
        mListAdapter.notifyDataSetChanged();
        return true;

      }
    });

    return super.onCreateOptionsMenu(menu);
  }

  @Override
  public boolean onPrepareOptionsMenu(Menu menu) {
    // Proper title for the grouping mode toggle item.
    if (mListAdapter.getGrouping() == MyExpandableListAdapter.GROUP_BY_ACTION) {
      mGroupingModeItem.setTitle(R.string.group_by_package);
      mGroupingModeItem.setIcon(R.drawable.ic_action_action_view_list);
    } else {
      mGroupingModeItem.setTitle(R.string.group_by_action);
      mGroupingModeItem.setIcon(R.drawable.ic_action_action_view_column);
    }

    // Decide whether we want to offer the option to collapse, or
    // expand, depending on the current group expansion count.
    ExpandableListView lv = getExpandableListView();
    int expandCount = 0;
    int numGroups = mListAdapter.getGroupCount();
    for (int i=numGroups-1; i>=0; i--) {
      if (lv.isGroupExpanded(i))
        expandCount += 1;
    }
    if (expandCount / (float)numGroups >= 0.5) {
      mExpandCollapseToggleItem.setTitle(R.string.collapse_all);
      mExpandCollapseToggleItem.setIcon(R.drawable.ic_action_navigation_expand_less);
      mExpandSuggested = false;
    }
    else {
      mExpandCollapseToggleItem.setTitle(R.string.expand_all);
      mExpandCollapseToggleItem.setIcon(R.drawable.ic_action_navigation_expand_more);
      mExpandSuggested = true;
    }

    // Reload button disabled while reloading
    mReloadItem.setEnabled(mLoadTask == null || mLoadTask.getStatus() != AsyncTask.Status.RUNNING);

    // View/Filter Submenu
    menu.findItem(R.id.view_changed_only).setChecked(mListAdapter.getShowChangedOnly());
    menu.findItem(R.id.view_hide_sys_apps).setChecked(mListAdapter.getFilterSystemApps());
    menu.findItem(R.id.view_hide_unknown).setChecked(mListAdapter.getFilterUnknown());

    return super.onPrepareOptionsMenu(menu);
  }

  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    case R.id.grouping:
      String groupingPref = "";
      if (mListAdapter.getGrouping() == MyExpandableListAdapter.GROUP_BY_ACTION) {
        mListAdapter.setGrouping(MyExpandableListAdapter.GROUP_BY_PACKAGE);
        groupingPref = "package";
      }
      else {
        mListAdapter.setGrouping(MyExpandableListAdapter.GROUP_BY_ACTION);
        groupingPref = "action";
      }
      mListAdapter.notifyDataSetChanged();
      mPrefs.edit().putString(PREF_GROUPING, groupingPref).commit();
      invalidateOptionsMenu();
      return true;

    case R.id.view_hide_sys_apps:
      item.setChecked(!item.isChecked());
      mListAdapter.setFilterSystemApps(item.isChecked());
      mListAdapter.notifyDataSetChanged();
      updateEmptyText();
      mPrefs.edit().putBoolean(ListActivity.PREF_FILTER_SYS_APPS, item.isChecked()).commit();
      return true;

    case R.id.view_changed_only:
      item.setChecked(!item.isChecked());
      mListAdapter.setShowChangedOnly(item.isChecked());
      mListAdapter.notifyDataSetChanged();
      updateEmptyText();
      mPrefs.edit().putBoolean(ListActivity.PREF_FILTER_SHOW_CHANGED, item.isChecked()).commit();
      return true;

    case R.id.view_hide_unknown:
      item.setChecked(!item.isChecked());
      mListAdapter.setFilterUnknown(item.isChecked());
      mListAdapter.notifyDataSetChanged();
      updateEmptyText();
      mPrefs.edit().putBoolean(ListActivity.PREF_FILTER_UNKNOWN, item.isChecked()).commit();
      return true;

    case R.id.expand:
      ExpandableListView lv = getExpandableListView();
      for (int i=mListAdapter.getGroupCount()-1; i>=0; i--)
        if (mExpandSuggested)
          lv.expandGroup(i);
        else
          lv.collapseGroup(i);
      invalidateOptionsMenu();
      return true;

    case R.id.reload:
      loadAndApply();
      return true;

    case R.id.help:
      startActivity(new Intent(this, HelpActivity.class));
      return true;

    default:
      return super.onContextItemSelected(item);
    }
  }

  @Override
  public boolean onChildClick(ExpandableListView parent, View v,
      int groupPosition, int childPosition, long id) {
    showEventDetails((IntentFilterInfo) mListAdapter.getChild(groupPosition, childPosition));
    return super.onChildClick(parent, v, groupPosition, childPosition, id);
  }

  void apply() {
    mListAdapter.setData(mEvents);
    mListAdapter.notifyDataSetChanged();
  }

  private void loadAndApply() {
    // Never start two tasks at once.
    if (mLoadTask != null && mLoadTask.getStatus() == AsyncTask.Status.RUNNING)
      return;

    mLoadTask = new LoadTask(this);
    mLoadTask.execute();
    invalidateOptionsMenu();
  }

  /**
   * The "loadIsFinished" argument is needed for the call from LoadTask,
   * because when it's postExecute() handler is run the task status
   * has not yet been switched to FINISHED (it's still RUNNING).
   */
  protected void updateEmptyText(boolean loadIsFinished) {
    // Once loading is done, the empty view switches from a progress bar to a text.
    TextView emptyText = (TextView) findViewById(R.id.empty_text);
    if (mLoadTask != null && mLoadTask.getStatus() == AsyncTask.Status.RUNNING && !loadIsFinished) {
      getExpandableListView().setEmptyView(findViewById((android.R.id.empty)));
    }
    else {
      getExpandableListView().setEmptyView(emptyText);
    }

    if (!mListAdapter.isFiltered())
      emptyText.setText(R.string.no_receivers);
    else if (!mListAdapter.getTextFilter().equals("")) {
      emptyText.setText(R.string.no_search_match);
    }
    else {
      // Unfortunately, we cannot link directly from the resource
      // string, and neither can we use {@see Linkify}. In both
      // cases, we end up with a URLSpan instance in the text (the
      // class is apparently hardcoded), which only allows link
      // resolving via Intent. We however want to use our own
      // URLSpan subclass which can trigger an event.
      //
      // We cannot override {@see Linkify} either, really - it
      // makes the one method we could override to easily inject
      // our custom subclass "final" - aaarg.
      CharSequence base = getString(R.string.no_receivers_filtered) +
        "\n\n";
      SpannableString full = new SpannableString(
          base +
          getString(R.string.change_filter_settings)
      );
      full.setSpan(new InternalURLSpan(new OnClickListener() {
          public void onClick(View v) {
            mActionBarMenu.performIdentifierAction(R.id.view, 0);
          }
        }), base.length(), full.length(),
        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      emptyText.setText(full);

      // Copied from "Linkify.addLinkMovementMethod" (which is private).
      MovementMethod m = emptyText.getMovementMethod();
      if ((m == null) || !(m instanceof LinkMovementMethod)) {
        if (emptyText.getLinksClickable()) {
          emptyText.setMovementMethod(LinkMovementMethod.getInstance());
        }
      }
    }
  }

  protected void updateEmptyText() {
    updateEmptyText(false);
  }

  protected void showEventDetails(IntentFilterInfo event) {
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    Fragment prev = getFragmentManager().findFragmentByTag("details");
    if (prev != null)
      ft.remove(prev);
    ft.addToBackStack(null);

    DialogFragment newFragment = EventDetailsFragment.newInstance(event);
    newFragment.show(ft, "details");
  }

  protected void addJob(ComponentInfo component, boolean newState) {
    Intent intent = new Intent(this, ToggleService.class);
    intent.putExtra("component", component);
    intent.putExtra("state", newState);
    startService(intent);
  }

  // TODO: Instead of showing a toast, fade in a custom info bar, then
  // fade out. This would be an improvement because we could control
  // it better: Show it longer, but have it disappear when the user
  // clicks on it (toasts don't receive clicks).
  // Consider: https://github.com/johnkil/Android-AppMsg
  public void showInfoToast(String action) {
    Object[] data = Actions.MAP.get(action);
    if (mInfoToast == null) {
      LayoutInflater inflater = getLayoutInflater();
      View layout = inflater.inflate(R.layout.detail_toast,
          (ViewGroup) findViewById(R.id.root));
      mInfoToast = new Toast(this);
      mInfoToast.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, 0, 0);
      mInfoToast.setDuration(Toast.LENGTH_LONG);
      mInfoToast.setView(layout);
    }
    ((TextView)mInfoToast.getView().findViewById(R.id.title)).setText(getIntentName(action));
    TextView message = ((TextView)mInfoToast.getView().findViewById(android.R.id.message));
    CharSequence info = "";
    if (data == null) {
      message.setVisibility(View.GONE);
    }
    else {
      if (data[2] != null)  // Hide info text both for null and empty string values.
        info = getResources().getText((Integer)data[2]);
      if (!info.equals("")) {
        message.setText(info);
        message.setVisibility(View.VISIBLE);
      } else {
        message.setVisibility(View.GONE);
      }
    }
    mInfoToast.show();
  }

  /**
   * Return a name for the given intent; tries the pretty name,
   * if available, and falls back to the raw class name.
   */
  String getIntentName(String action) {
    Object[] data = Actions.MAP.get(action);
    if (data == null)
      return action;
    return (data[1] != null) ?
        getResources().getString((Integer)data[1]) :
        (String)data[0];
  }

  /**
   * URLSpan class that supports a custom onClick listener, rather than
   * sending everything through an Intent.
   */
  static class InternalURLSpan extends ClickableSpan {
    OnClickListener mListener;

    public InternalURLSpan(OnClickListener listener) {
      mListener = listener;
    }

    @Override
    public void onClick(View widget) {
      mListener.onClick(widget);
    }
  }
}




Java Source Code List

com.elsdoerfer.android.autostarts.Actions.java
com.elsdoerfer.android.autostarts.DatabaseHelper.java
com.elsdoerfer.android.autostarts.EventDetailsFragment.java
com.elsdoerfer.android.autostarts.HelpActivity.java
com.elsdoerfer.android.autostarts.ListActivity.java
com.elsdoerfer.android.autostarts.LoadTask.java
com.elsdoerfer.android.autostarts.MyExpandableListAdapter.java
com.elsdoerfer.android.autostarts.ReceiverReader.java
com.elsdoerfer.android.autostarts.ToggleService.java
com.elsdoerfer.android.autostarts.ToggleTool.java
com.elsdoerfer.android.autostarts.Utils.java
com.elsdoerfer.android.autostarts.compat.FixedExpandableListView.java
com.elsdoerfer.android.autostarts.db.ComponentInfo.java
com.elsdoerfer.android.autostarts.db.IntentFilterInfo.java
com.elsdoerfer.android.autostarts.db.PackageInfo.java
com.stericson.RootTools.Constants.java
com.stericson.RootTools.RootTools.java
com.stericson.RootTools.containers.Mount.java
com.stericson.RootTools.containers.Permissions.java
com.stericson.RootTools.containers.RootClass.java
com.stericson.RootTools.containers.Symlink.java
com.stericson.RootTools.exceptions.RootDeniedException.java
com.stericson.RootTools.execution.CommandCapture.java
com.stericson.RootTools.execution.Command.java
com.stericson.RootTools.execution.JavaCommandCapture.java
com.stericson.RootTools.execution.Shell.java
com.stericson.RootTools.internal.Installer.java
com.stericson.RootTools.internal.InternalVariables.java
com.stericson.RootTools.internal.Remounter.java
com.stericson.RootTools.internal.RootToolsInternalMethods.java
com.stericson.RootTools.internal.Runner.java
src.com.elsdoerfer.android.autostarts.opt.MarketUtils.java
src.com.elsdoerfer.android.autostarts.opt.MarketUtils.java
src.com.elsdoerfer.android.autostarts.opt.RootFeatures.java
src.com.elsdoerfer.android.autostarts.opt.RootFeatures.java