package de.robv.android.xposed.mods.appsettings; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.regex.Pattern; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityManager.RecentTaskInfo; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PermissionInfo; import android.graphics.Color; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.text.method.LinkMovementMethod; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Filter; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.LinearLayout.LayoutParams; import android.widget.ListView; import android.widget.SearchView; import android.widget.SectionIndexer; import android.widget.SimpleAdapter; import android.widget.TextView; import android.widget.Toast; import de.robv.android.xposed.mods.appsettings.FilterItemComponent.FilterState; import de.robv.android.xposed.mods.appsettings.settings.ApplicationSettings; import de.robv.android.xposed.mods.appsettings.settings.PermissionsListAdapter; @SuppressLint("WorldReadableFiles") public class XposedModActivity extends Activity { private ArrayList<ApplicationInfo> appList = new ArrayList<ApplicationInfo>(); private ArrayList<ApplicationInfo> filteredAppList = new ArrayList<ApplicationInfo>(); private Map<String, Set<String>> permUsage = new HashMap<String, Set<String>>(); private Map<String, Set<String>> sharedUsers = new HashMap<String, Set<String>>(); private Map<String, String> pkgSharedUsers = new HashMap<String, String>(); private String nameFilter; private FilterState filterAppType; private FilterState filterAppState; private FilterState filterActive; private String filterPermissionUsage; private List<SettingInfo> settings; private static File prefsFile = new File(Environment.getDataDirectory(), "data/" + Common.MY_PACKAGE_NAME + "/shared_prefs/" + Common.PREFS + ".xml"); private static File backupPrefsFile = new File(Environment.getExternalStorageDirectory(), "AppSettings-Backup.xml"); private SharedPreferences prefs; @Override public void onCreate(Bundle savedInstanceState) { setTitle(R.string.app_name); super.onCreate(savedInstanceState); prefsFile.setReadable(true, false); prefs = getSharedPreferences(Common.PREFS, Context.MODE_WORLD_READABLE); loadSettings(); setContentView(R.layout.main); ListView list = (ListView) findViewById(R.id.lstApps); registerForContextMenu(list); list.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // Open settings activity when clicking on an application String pkgName = ((TextView) view.findViewById(R.id.app_package)).getText().toString(); Intent i = new Intent(getApplicationContext(), ApplicationSettings.class); i.putExtra("package", pkgName); startActivityForResult(i, position); } }); refreshApps(); } private void loadSettings() { settings = new ArrayList<SettingInfo>(); settings.add(new SettingInfo(Common.PREF_DPI, getString(R.string.settings_dpi))); settings.add(new SettingInfo(Common.PREF_FONT_SCALE, getString(R.string.settings_fontscale))); settings.add(new SettingInfo(Common.PREF_SCREEN, getString(R.string.settings_screen))); settings.add(new SettingInfo(Common.PREF_XLARGE, getString(R.string.settings_xlargeres))); settings.add(new SettingInfo(Common.PREF_RES_ON_WIDGETS, getString(R.string.settings_resonwidgets))); settings.add(new SettingInfo(Common.PREF_LOCALE, getString(R.string.settings_locale))); settings.add(new SettingInfo(Common.PREF_FULLSCREEN, getString(R.string.settings_fullscreen))); settings.add(new SettingInfo(Common.PREF_NO_TITLE, getString(R.string.settings_notitle))); settings.add(new SettingInfo(Common.PREF_SCREEN_ON, getString(R.string.settings_screenon))); settings.add(new SettingInfo(Common.PREF_ALLOW_ON_LOCKSCREEN, getString(R.string.settings_showwhenlocked))); settings.add(new SettingInfo(Common.PREF_RESIDENT, getString(R.string.settings_resident))); settings.add(new SettingInfo(Common.PREF_NO_FULLSCREEN_IME, getString(R.string.settings_nofullscreenime))); settings.add(new SettingInfo(Common.PREF_ORIENTATION, getString(R.string.settings_orientation))); settings.add(new SettingInfo(Common.PREF_INSISTENT_NOTIF, getString(R.string.settings_insistentnotif))); settings.add(new SettingInfo(Common.PREF_NO_BIG_NOTIFICATIONS, getString(R.string.settings_nobignotif))); settings.add(new SettingInfo(Common.PREF_ONGOING_NOTIF, getString(R.string.settings_ongoingnotif))); settings.add(new SettingInfo(Common.PREF_NOTIF_PRIORITY, getString(R.string.settings_notifpriority))); settings.add(new SettingInfo(Common.PREF_RECENTS_MODE, getString(R.string.settings_recents_mode))); settings.add(new SettingInfo(Common.PREF_MUTE, getString(R.string.settings_mute))); settings.add(new SettingInfo(Common.PREF_LEGACY_MENU, getString(R.string.settings_legacy_menu))); settings.add(new SettingInfo(Common.PREF_REVOKEPERMS, getString(R.string.settings_permissions))); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); // Refresh the app that was just edited, if it's visible in the list ListView list = (ListView) findViewById(R.id.lstApps); if (requestCode >= list.getFirstVisiblePosition() && requestCode <= list.getLastVisiblePosition()) { View v = list.getChildAt(requestCode - list.getFirstVisiblePosition()); list.getAdapter().getView(requestCode, v, list); } else if (requestCode == Integer.MAX_VALUE) { list.invalidateViews(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_refresh: refreshApps(); return true; case R.id.menu_recents: showRecents(); return true; case R.id.menu_export: doExport(); return true; case R.id.menu_import: doImport(); return true; case R.id.menu_about: showAboutDialog(); return true; default: return super.onOptionsItemSelected(item); } } private void refreshApps() { appList.clear(); // (re)load the list of apps in the background new PrepareAppsAdapter().execute(); } private void showRecents() { ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); PackageManager pm = getPackageManager(); final List<Map<String, Object>> data = new ArrayList<Map<String, Object>>(); for (RecentTaskInfo task : am.getRecentTasks(30, ActivityManager.RECENT_WITH_EXCLUDED)) { Intent i = task.baseIntent; if (i.getComponent() == null) continue; Map<String, Object> entry = new HashMap<String, Object>(); try { entry.put("image", pm.getActivityIcon(i)); } catch (NameNotFoundException e) { entry.put("image", pm.getDefaultActivityIcon()); } try { entry.put("label", pm.getActivityInfo(i.getComponent(), 0).loadLabel(pm).toString()); } catch (NameNotFoundException e) { entry.put("label", ""); } entry.put("package", i.getComponent().getPackageName()); data.add(entry); } String[] from = new String[] { "image", "label", "package" }; int[] to = new int[] { R.id.recent_icon, R.id.recent_label, R.id.recent_package }; SimpleAdapter adapter = new SimpleAdapter(this, data, R.layout.recent_item, from, to); adapter.setViewBinder(new SimpleAdapter.ViewBinder() { @Override public boolean setViewValue(final View view, final Object data, final String textRepresentation) { if (view instanceof ImageView) { ((ImageView) view).setImageDrawable((Drawable) data); return true; } return false; } }); new AlertDialog.Builder(this) .setTitle(R.string.recents_title) .setAdapter(adapter, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent i = new Intent(getApplicationContext(), ApplicationSettings.class); i.putExtra("package", (String) data.get(which).get("package")); startActivityForResult(i, Integer.MAX_VALUE); } }) .show(); } private void doExport() { new ExportTask().execute(backupPrefsFile); } private void doImport() { if (!backupPrefsFile.exists()) { Toast.makeText(this, getString(R.string.imp_exp_file_doesnt_exist, backupPrefsFile.getAbsolutePath()), Toast.LENGTH_LONG).show(); return; } AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.menu_import); builder.setMessage(R.string.imp_exp_confirm); builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); new ImportTask().execute(backupPrefsFile); } }); builder.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Do nothing dialog.dismiss(); } }); AlertDialog alert = builder.create(); alert.show(); } private class ExportTask extends AsyncTask<File, String, String> { @Override protected String doInBackground(File... params) { File outFile = params[0]; try { copyFile(prefsFile, outFile); return getString(R.string.imp_exp_exported, outFile.getAbsolutePath()); } catch (IOException ex) { return getString(R.string.imp_exp_export_error, ex.getMessage()); } } @Override protected void onPostExecute(String result) { Toast.makeText(XposedModActivity.this, result, Toast.LENGTH_LONG).show(); } } private class ImportTask extends AsyncTask<File, String, String> { private boolean importSuccessful; @Override protected String doInBackground(File... params) { importSuccessful = false; File inFile = params[0]; String tempFilename = Common.PREFS + "-new"; File newPrefsFile = new File(prefsFile.getParentFile(), tempFilename + ".xml"); // Make sure the shared_prefs folder exists, with the proper permissions getSharedPreferences(tempFilename, Context.MODE_WORLD_READABLE).edit().commit(); try { copyFile(inFile, newPrefsFile); } catch (IOException ex) { return getString(R.string.imp_exp_import_error, ex.getMessage()); } newPrefsFile.setReadable(true, false); SharedPreferences newPrefs = getSharedPreferences(tempFilename, Context.MODE_WORLD_READABLE | Context.MODE_MULTI_PROCESS); if (newPrefs.getAll().size() == 0) { // No entries in imported file, discard it newPrefsFile.delete(); return getString(R.string.imp_exp_invalid_import_file, inFile.getAbsoluteFile()); } else { if (newPrefsFile.renameTo(prefsFile)) { importSuccessful = true; } else { prefsFile.delete(); if (newPrefsFile.renameTo(prefsFile)) importSuccessful = true; } return getString(R.string.imp_exp_imported); } } @Override protected void onPostExecute(String result) { if (importSuccessful) { // Refresh preferences prefs = getSharedPreferences(Common.PREFS, Context.MODE_WORLD_READABLE | Context.MODE_MULTI_PROCESS); // Refresh listed apps (account for filters) AppListAdapter appListAdapter = (AppListAdapter) ((ListView) findViewById(R.id.lstApps)).getAdapter(); appListAdapter.getFilter().filter(nameFilter); } Toast.makeText(XposedModActivity.this, result, Toast.LENGTH_LONG).show(); } } private static void copyFile(File source, File dest) throws IOException { InputStream in = null; OutputStream out = null; boolean success = false; try { in = new FileInputStream(source); out = new FileOutputStream(dest); byte[] buf = new byte[10 * 1024]; int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } out.flush(); out.close(); out = null; success = true; } catch (IOException ex) { throw ex; } finally { if (in != null) { try { in.close(); } catch (Exception ex) { } } if (out != null) { try { out.close(); } catch (Exception ex) { } } if (!success) { dest.delete(); } } } private void showAboutDialog() { View vAbout; vAbout = getLayoutInflater().inflate(R.layout.about, null); // Warn if the module is not active if (!isModActive()) vAbout.findViewById(R.id.about_notactive).setVisibility(View.VISIBLE); // Display the resources translator, or hide it if none String translator = getResources().getString(R.string.translator); TextView txtTranslation = (TextView) vAbout.findViewById(R.id.about_translation); if (translator.isEmpty()) { txtTranslation.setVisibility(View.GONE); } else { txtTranslation.setText(getString(R.string.app_translation, translator)); txtTranslation.setMovementMethod(LinkMovementMethod.getInstance()); } // Clickable links ((TextView) vAbout.findViewById(R.id.about_title)).setMovementMethod(LinkMovementMethod.getInstance()); // Display the correct version try { ((TextView) vAbout.findViewById(R.id.version)).setText(getString(R.string.app_version, getPackageManager().getPackageInfo(getPackageName(), 0).versionName)); } catch (NameNotFoundException e) { } // Prepare and show the dialog Builder dlgBuilder = new AlertDialog.Builder(this); dlgBuilder.setTitle(R.string.app_name); dlgBuilder.setCancelable(true); dlgBuilder.setIcon(R.drawable.ic_launcher); dlgBuilder.setPositiveButton(android.R.string.ok, null); dlgBuilder.setView(vAbout); dlgBuilder.show(); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { if (v.getId() == R.id.lstApps) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; ApplicationInfo appInfo = filteredAppList.get(info.position); menu.setHeaderTitle(getPackageManager().getApplicationLabel(appInfo)); getMenuInflater().inflate(R.menu.menu_app, menu); menu.findItem(R.id.menu_save).setVisible(false); ApplicationSettings.updateMenuEntries(getApplicationContext(), menu, appInfo.packageName); } else { super.onCreateContextMenu(menu, v, menuInfo); } } @Override public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); String pkgName = filteredAppList.get(info.position).packageName; if (item.getItemId() == R.id.menu_app_launch) { Intent LaunchIntent = getPackageManager().getLaunchIntentForPackage(pkgName); startActivity(LaunchIntent); return true; } else if (item.getItemId() == R.id.menu_app_settings) { startActivity(new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + pkgName))); return true; } else if (item.getItemId() == R.id.menu_app_store) { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + pkgName))); return true; } return super.onContextItemSelected(item); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_SEARCH && (event.getFlags() & KeyEvent.FLAG_CANCELED) == 0) { SearchView searchV = (SearchView) findViewById(R.id.searchApp); if (searchV.isShown()) { searchV.setIconified(false); return true; } } return super.onKeyUp(keyCode, event); } private static boolean isModActive() { return false; } @SuppressLint("DefaultLocale") private void loadApps(ProgressDialog dialog) { appList.clear(); permUsage.clear(); sharedUsers.clear(); pkgSharedUsers.clear(); PackageManager pm = getPackageManager(); List<PackageInfo> pkgs = getPackageManager().getInstalledPackages(PackageManager.GET_PERMISSIONS); dialog.setMax(pkgs.size()); int i = 1; for (PackageInfo pkgInfo : pkgs) { dialog.setProgress(i++); ApplicationInfo appInfo = pkgInfo.applicationInfo; if (appInfo == null) continue; appInfo.name = appInfo.loadLabel(pm).toString(); appList.add(appInfo); String[] perms = pkgInfo.requestedPermissions; if (perms != null) for (String perm : perms) { Set<String> permUsers = permUsage.get(perm); if (permUsers == null) { permUsers = new TreeSet<String>(); permUsage.put(perm, permUsers); } permUsers.add(pkgInfo.packageName); } if (pkgInfo.sharedUserId != null) { Set<String> sharedUserPackages = sharedUsers.get(pkgInfo.sharedUserId); if (sharedUserPackages == null) { sharedUserPackages = new TreeSet<String>(); sharedUsers.put(pkgInfo.sharedUserId, sharedUserPackages); } sharedUserPackages.add(pkgInfo.packageName); pkgSharedUsers.put(pkgInfo.packageName, pkgInfo.sharedUserId); } } Collections.sort(appList, new Comparator<ApplicationInfo>() { @Override public int compare(ApplicationInfo lhs, ApplicationInfo rhs) { if (lhs.name == null) { return -1; } else if (rhs.name == null) { return 1; } else { return lhs.name.toUpperCase().compareTo(rhs.name.toUpperCase()); } } }); } private void prepareAppList() { final AppListAdapter appListAdapter = new AppListAdapter(XposedModActivity.this, appList); ((ListView) findViewById(R.id.lstApps)).setAdapter(appListAdapter); appListAdapter.getFilter().filter(nameFilter); ((SearchView) findViewById(R.id.searchApp)).setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { nameFilter = query; appListAdapter.getFilter().filter(nameFilter); ((SearchView) findViewById(R.id.searchApp)).clearFocus(); return false; } @Override public boolean onQueryTextChange(String newText) { nameFilter = newText; appListAdapter.getFilter().filter(nameFilter); return false; } }); ((ImageButton) findViewById(R.id.btnFilter)).setOnClickListener(new View.OnClickListener() { Dialog filterDialog; Map<String, FilterItemComponent> filterComponents; @Override public void onClick(View v) { // set up dialog filterDialog = new Dialog(XposedModActivity.this); filterDialog.setContentView(R.layout.filter_dialog); filterDialog.setTitle(R.string.filter_title); filterDialog.setCancelable(true); filterDialog.setOwnerActivity(XposedModActivity.this); LinearLayout entriesView = (LinearLayout) filterDialog.findViewById(R.id.filter_entries); filterComponents = new HashMap<String, FilterItemComponent>(); for (SettingInfo setting : settings) { FilterItemComponent component = new FilterItemComponent(XposedModActivity.this, setting.label, null, null, null); component.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); component.setFilterState(setting.filter); entriesView.addView(component); filterComponents.put(setting.settingKey, component); } ((FilterItemComponent) filterDialog.findViewById(R.id.fltAppType)).setFilterState(filterAppType); ((FilterItemComponent) filterDialog.findViewById(R.id.fltAppState)).setFilterState(filterAppState); ((FilterItemComponent) filterDialog.findViewById(R.id.fltActive)).setFilterState(filterActive); // Block or unblock the details based on the Active setting enableFilterDetails(!FilterState.UNCHANGED.equals(filterActive)); ((FilterItemComponent) filterDialog.findViewById(R.id.fltActive)).setOnFilterChangeListener(new FilterItemComponent.OnFilterChangeListener() { @Override public void onFilterChanged(FilterItemComponent item, FilterState state) { enableFilterDetails(!FilterState.UNCHANGED.equals(state)); } }); // Close the dialog with the possible options ((Button) filterDialog.findViewById(R.id.btnFilterCancel)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { filterDialog.dismiss(); } }); ((Button) filterDialog.findViewById(R.id.btnFilterClear)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { filterAppType = FilterState.ALL; filterAppState = FilterState.ALL; filterActive = FilterState.ALL; for (SettingInfo setting : settings) setting.filter = FilterState.ALL; filterDialog.dismiss(); appListAdapter.getFilter().filter(nameFilter); } }); ((Button) filterDialog.findViewById(R.id.btnFilterApply)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { filterAppType = ((FilterItemComponent) filterDialog.findViewById(R.id.fltAppType)).getFilterState(); filterAppState = ((FilterItemComponent) filterDialog.findViewById(R.id.fltAppState)).getFilterState(); filterActive = ((FilterItemComponent) filterDialog.findViewById(R.id.fltActive)).getFilterState(); for (SettingInfo setting : settings) setting.filter = filterComponents.get(setting.settingKey).getFilterState(); filterDialog.dismiss(); appListAdapter.getFilter().filter(nameFilter); } }); filterDialog.show(); } private void enableFilterDetails(boolean enable) { for (FilterItemComponent component : filterComponents.values()) component.setEnabled(enable); } }); ((ImageButton) findViewById(R.id.btnPermsFilter)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { AlertDialog.Builder bld = new AlertDialog.Builder(XposedModActivity.this); bld.setCancelable(true); bld.setTitle(R.string.perms_filter_title); List<String> perms = new LinkedList<String>(permUsage.keySet()); Collections.sort(perms); List<PermissionInfo> items = new ArrayList<PermissionInfo>(); PackageManager pm = getPackageManager(); for (String perm : perms) { try { items.add(pm.getPermissionInfo(perm, 0)); } catch (NameNotFoundException e) { PermissionInfo unknownPerm = new PermissionInfo(); unknownPerm.name = perm; items.add(unknownPerm); } } final PermissionsListAdapter adapter = new PermissionsListAdapter(XposedModActivity.this, items, new HashSet<String>(), false); bld.setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { filterPermissionUsage = adapter.getItem(which).name; appListAdapter.getFilter().filter(nameFilter); } }); final View permsView = getLayoutInflater().inflate(R.layout.permission_search, null); ((SearchView) permsView.findViewById(R.id.searchPermission)).setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { adapter.getFilter().filter(query); ((SearchView) permsView.findViewById(R.id.searchPermission)).clearFocus(); return false; } @Override public boolean onQueryTextChange(String newText) { adapter.getFilter().filter(newText); return false; } }); bld.setView(permsView); bld.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { filterPermissionUsage = null; appListAdapter.getFilter().filter(nameFilter); } }); AlertDialog dialog = bld.create(); dialog.getListView().setFastScrollEnabled(true); dialog.show(); } }); } // Handle background loading of apps private class PrepareAppsAdapter extends AsyncTask<Void,Void,AppListAdapter> { ProgressDialog dialog; @Override protected void onPreExecute() { dialog = new ProgressDialog(((ListView) findViewById(R.id.lstApps)).getContext()); dialog.setMessage(getString(R.string.app_loading)); dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); dialog.setCancelable(false); dialog.show(); } @Override protected AppListAdapter doInBackground(Void... params) { if (appList.size() == 0) { loadApps(dialog); } return null; } @Override protected void onPostExecute(final AppListAdapter result) { prepareAppList(); try { dialog.dismiss(); } catch (Exception e) { } } } /** Hold filter state and other info for each setting key */ private static class SettingInfo { String settingKey; String label; FilterState filter; SettingInfo(String setting, String label) { this.settingKey = setting; this.label = label; filter = FilterState.ALL; } } private class AppListFilter extends Filter { private AppListAdapter adapter; AppListFilter(AppListAdapter adapter) { super(); this.adapter = adapter; } @Override protected FilterResults performFiltering(CharSequence constraint) { // NOTE: this function is *always* called from a background thread, and // not the UI thread. ArrayList<ApplicationInfo> items = new ArrayList<ApplicationInfo>(); synchronized (this) { items.addAll(appList); } SharedPreferences prefs = getSharedPreferences(Common.PREFS, Context.MODE_WORLD_READABLE); FilterResults result = new FilterResults(); if (constraint != null && constraint.length() > 0) { Pattern regexp = Pattern.compile(constraint.toString(), Pattern.LITERAL | Pattern.CASE_INSENSITIVE); for (Iterator<ApplicationInfo> i = items.iterator(); i.hasNext(); ) { ApplicationInfo app = i.next(); if (!regexp.matcher(app.name == null ? "" : app.name).find() && !regexp.matcher(app.packageName).find()) { i.remove(); } } } for (Iterator<ApplicationInfo> i = items.iterator(); i.hasNext(); ) { ApplicationInfo app = i.next(); if (filteredOut(prefs, app)) i.remove(); } result.values = items; result.count = items.size(); return result; } private boolean filteredOut(SharedPreferences prefs, ApplicationInfo app) { String packageName = app.packageName; boolean isUser = (app.flags & ApplicationInfo.FLAG_SYSTEM) == 0; // AppType = Overridden is used for USER apps if (filteredOut(isUser, filterAppType)) return true; // AppState = Overridden is used for ENABLED apps if (filteredOut(app.enabled, filterAppState)) return true; if (filteredOut(prefs.getBoolean(packageName + Common.PREF_ACTIVE, false), filterActive)) return true; if (FilterState.UNCHANGED.equals(filterActive)) // Ignore additional filters return false; for (SettingInfo setting : settings) if (filteredOut(prefs.contains(packageName + setting.settingKey), setting.filter)) return true; if (filterPermissionUsage != null) { Set<String> pkgsForPerm = permUsage.get(filterPermissionUsage); if (!pkgsForPerm.contains(packageName)) return true; } return false; } private boolean filteredOut(boolean set, FilterState state) { if (state == null) return false; switch (state) { case UNCHANGED: return set; case OVERRIDDEN: return !set; default: return false; } } @SuppressWarnings("unchecked") @Override protected void publishResults(CharSequence constraint, FilterResults results) { // NOTE: this function is *always* called from the UI thread. filteredAppList = (ArrayList<ApplicationInfo>) results.values; adapter.notifyDataSetChanged(); adapter.clear(); for (int i = 0, l = filteredAppList.size(); i < l; i++) { adapter.add(filteredAppList.get(i)); } adapter.notifyDataSetInvalidated(); } } static class AppListViewHolder { TextView app_name; TextView app_package; ImageView app_icon; AsyncTask<AppListViewHolder, Void, Drawable> imageLoader; } class AppListAdapter extends ArrayAdapter<ApplicationInfo> implements SectionIndexer { private Map<String, Integer> alphaIndexer; private String[] sections; private Filter filter; private LayoutInflater inflater; private Drawable defaultIcon; @SuppressLint("DefaultLocale") public AppListAdapter(Context context, List<ApplicationInfo> items) { super(context, R.layout.app_list_item, new ArrayList<ApplicationInfo>(items)); filteredAppList.addAll(items); filter = new AppListFilter(this); inflater = getLayoutInflater(); defaultIcon = getResources().getDrawable(android.R.drawable.sym_def_app_icon); alphaIndexer = new HashMap<String, Integer>(); for (int i = filteredAppList.size() - 1; i >= 0; i--) { ApplicationInfo app = filteredAppList.get(i); String appName = app.name; String firstChar; if (appName == null || appName.length() < 1) { firstChar = "@"; } else { firstChar = appName.substring(0, 1).toUpperCase(); if (firstChar.charAt(0) > 'Z' || firstChar.charAt(0) < 'A') firstChar = "@"; } alphaIndexer.put(firstChar, i); } Set<String> sectionLetters = alphaIndexer.keySet(); // create a list from the set to sort List<String> sectionList = new ArrayList<String>(sectionLetters); Collections.sort(sectionList); sections = new String[sectionList.size()]; sectionList.toArray(sections); } @Override public View getView(int position, View convertView, ViewGroup parent) { // Load or reuse the view for this row View row = convertView; AppListViewHolder holder; if (row == null) { row = inflater.inflate(R.layout.app_list_item, parent, false); holder = new AppListViewHolder(); holder.app_name = (TextView) row.findViewById(R.id.app_name); holder.app_package = (TextView) row.findViewById(R.id.app_package); holder.app_icon = (ImageView) row.findViewById(R.id.app_icon); row.setTag(holder); } else { holder = (AppListViewHolder) row.getTag(); holder.imageLoader.cancel(true); } final ApplicationInfo app = filteredAppList.get(position); holder.app_name.setText(app.name == null ? "" : app.name); holder.app_package.setTextColor(prefs.getBoolean(app.packageName + Common.PREF_ACTIVE, false) ? Color.RED : Color.parseColor("#0099CC")); holder.app_package.setText(app.packageName); holder.app_icon.setImageDrawable(defaultIcon); if (app.enabled) { holder.app_name.setPaintFlags(holder.app_name.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); holder.app_package.setPaintFlags(holder.app_package.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG); } else { holder.app_name.setPaintFlags(holder.app_name.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); holder.app_package.setPaintFlags(holder.app_package.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); } holder.imageLoader = new AsyncTask<AppListViewHolder, Void, Drawable>() { private AppListViewHolder v; @Override protected Drawable doInBackground(AppListViewHolder... params) { v = params[0]; return app.loadIcon(getPackageManager()); } @Override protected void onPostExecute(Drawable result) { v.app_icon.setImageDrawable(result); } }.execute(holder); return row; } @SuppressLint("DefaultLocale") @Override public void notifyDataSetInvalidated() { alphaIndexer.clear(); for (int i = filteredAppList.size() - 1; i >= 0; i--) { ApplicationInfo app = filteredAppList.get(i); String appName = app.name; String firstChar; if (appName == null || appName.length() < 1) { firstChar = "@"; } else { firstChar = appName.substring(0, 1).toUpperCase(); if (firstChar.charAt(0) > 'Z' || firstChar.charAt(0) < 'A') firstChar = "@"; } alphaIndexer.put(firstChar, i); } Set<String> keys = alphaIndexer.keySet(); Iterator<String> it = keys.iterator(); ArrayList<String> keyList = new ArrayList<String>(); while (it.hasNext()) { keyList.add(it.next()); } Collections.sort(keyList); sections = new String[keyList.size()]; keyList.toArray(sections); super.notifyDataSetInvalidated(); } @Override public int getPositionForSection(int section) { if (section >= sections.length) return filteredAppList.size() - 1; return alphaIndexer.get(sections[section]); } @Override public int getSectionForPosition(int position) { // Iterate over the sections to find the closest index // that is not greater than the position int closestIndex = 0; int latestDelta = Integer.MAX_VALUE; for (int i = 0; i < sections.length; i++) { int current = alphaIndexer.get(sections[i]); if (current == position) { // If position matches an index, return it immediately return i; } else if (current < position) { // Check if this is closer than the last index we inspected int delta = position - current; if (delta < latestDelta) { closestIndex = i; latestDelta = delta; } } } return closestIndex; } @Override public Object[] getSections() { return sections; } @Override public Filter getFilter() { return filter; } } }