Java tutorial
/* * IntentsLab - Android app for playing with Intents and Binder IPC * Copyright (C) 2014 Micha Bednarski * * 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 3 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, see <http://www.gnu.org/licenses/>. */ package com.github.michalbednarski.intentslab.editor; import android.app.AlertDialog; import android.app.Dialog; import android.content.ComponentName; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ComponentInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Parcelable; import android.support.v4.app.DialogFragment; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import com.github.michalbednarski.intentslab.R; import com.github.michalbednarski.intentslab.SingleFragmentActivity; import com.github.michalbednarski.intentslab.Utils; import com.github.michalbednarski.intentslab.browser.ComponentInfoFragment; import com.github.michalbednarski.intentslab.browser.ExtendedPackageInfo; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; /** * Dialog for picking app by inexact intent filters. */ public class FindComponentDialog extends DialogFragment implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener, View.OnClickListener, DialogInterface.OnKeyListener { /** * ListAdapter for list of apps/components */ private final AppListAdapter mAppListAdapter = new AppListAdapter(); /** * Flag that scanning is finished. * * May be set back to false from true. * Must be modified only by {@link ListFiltersTask} */ private boolean mIsScanningFinished = false; /** * Intent for which we look for IntentFilters */ private Intent mIntent = null; /** * Flag for {@link #mFlags} * When scanning IntentFilters ignore case and match just substrings from values */ private static final int FLAG_CASE_INSENSITIVE_AND_SUBSTRING = 1; private static final int FLAG_TEST_ACTION = 2; private static final int FLAG_TEST_CATEGORIES = 4; /** * Currently selected filtering flags. * To get used filtering options bit-and this value with {@link #mAvailableFlags} */ private int mFlags = FLAG_CASE_INSENSITIVE_AND_SUBSTRING | FLAG_TEST_ACTION | FLAG_TEST_CATEGORIES; /** * Flags that can be used and shown to user in filter options */ private int mAvailableFlags = 0; /** * Package from which we're displaying components or null if from all */ private AppWithMatchingFilters mInPackage = null; private View mProgressView; private ListView mListView; private Parcelable mAllAppsListViewState; public FindComponentDialog() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Enable instance retaining, we'll continue scanning when configuration changes setRetainInstance(true); // Get intent final IntentEditorActivity intentEditor = (IntentEditorActivity) getActivity(); mIntent = intentEditor.getEditedIntent(); // Before showing dialog IntentEditorActivity updates Intent // Calculate available flags if (mIntent.getAction() != null && !"".equals(mIntent.getAction())) { mAvailableFlags |= FLAG_TEST_ACTION | FLAG_CASE_INSENSITIVE_AND_SUBSTRING; } if (mIntent.getCategories() != null && !mIntent.getCategories().isEmpty()) { mAvailableFlags |= FLAG_TEST_CATEGORIES | FLAG_CASE_INSENSITIVE_AND_SUBSTRING; } // Start scanning (new ListFiltersTask()).execute(); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Dialog dialog = super.onCreateDialog(savedInstanceState); dialog.setTitle(R.string.find); dialog.setOnKeyListener(this); return dialog; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.attach_intent_filter, container, false); mProgressView = view.findViewById(R.id.progress); // Prepare ListView mListView = (ListView) view.findViewById(R.id.apps_list_view); mListView.setOnItemClickListener(this); mListView.setOnItemLongClickListener(this); Utils.fixListViewInDialogBackground(mListView); if (mIsScanningFinished) { hideProgressAndShowList(); } // Hide unneeded button or attach events Button optionsButton = (Button) view.findViewById(R.id.options); if (mAvailableFlags == 0) { optionsButton.setVisibility(View.GONE); } else { optionsButton.setOnClickListener(this); } return view; } void hideProgressAndShowList() { if (mListView != null) { mProgressView.setVisibility(View.GONE); mListView.setVisibility(View.VISIBLE); mListView.setAdapter(mAppListAdapter); } } @Override public void onDestroyView() { mProgressView = null; mListView.setAdapter(null); mListView = null; try { // Work around dismiss on rotation after setRetainInstance(true) // http://stackoverflow.com/a/13596466 getDialog().setOnDismissListener(null); } catch (Exception ignored) { } super.onDestroyView(); } /** * The item was clicked. * * Set IntentFilters in activity and dismiss() */ @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { mAppListAdapter.getItem(position).handleClick(); } /** * Item on list was long clicked. * * Show app details */ @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { ListItem item = mAppListAdapter.getItem(position); if (item instanceof ComponentWithMatchingFilters) { ComponentInfo info = ((ComponentWithMatchingFilters) item).componentInfo; startActivity(new Intent(getActivity(), SingleFragmentActivity.class) .putExtra(SingleFragmentActivity.EXTRA_FRAGMENT, ComponentInfoFragment.class.getName()) .putExtra(ComponentInfoFragment.ARG_PACKAGE_NAME, info.packageName) .putExtra(ComponentInfoFragment.ARG_COMPONENT_NAME, info.name) .putExtra(ComponentInfoFragment.ARG_LAUNCHED_FROM_INTENT_EDITOR, true)); return true; } return false; } /** * Filter options button was pressed */ @Override public void onClick(View v) { (new OptionsAlertDialog()).show(); } /** * Handle back key if we're in specific app */ @Override public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && mInPackage != null) { mInPackage = null; mAppListAdapter.notifyDataSetChanged(); if (mAllAppsListViewState != null) { mListView.onRestoreInstanceState(mAllAppsListViewState); } return true; } return false; } private class OptionsAlertDialog implements DialogInterface.OnMultiChoiceClickListener, DialogInterface.OnClickListener { ArrayList<String> mOptionNames = new ArrayList<String>(); ArrayList<Integer> mOptionFlags = new ArrayList<Integer>(); int mNewFlags = mFlags; OptionsAlertDialog() { // Add options if they are available if ((mAvailableFlags & FLAG_TEST_ACTION) != 0) { mOptionNames.add(getActivity().getString(R.string.filters_filter_test_action)); mOptionFlags.add(FLAG_TEST_ACTION); } if ((mAvailableFlags & FLAG_TEST_CATEGORIES) != 0) { mOptionNames.add(getActivity().getString(R.string.filters_filter_test_categories)); mOptionFlags.add(FLAG_TEST_CATEGORIES); } if ((mAvailableFlags & FLAG_CASE_INSENSITIVE_AND_SUBSTRING) != 0) { mOptionNames.add(getActivity().getString(R.string.filters_filter_substring_and_insensitive)); mOptionFlags.add(FLAG_CASE_INSENSITIVE_AND_SUBSTRING); } } void show() { // Get current values of flags boolean[] values = new boolean[mOptionFlags.size()]; int i = 0; for (Integer optionFlag : mOptionFlags) { values[i++] = (mNewFlags & optionFlag) != 0; } // Create and show dialog new AlertDialog.Builder(getActivity()) .setMultiChoiceItems(mOptionNames.toArray(new CharSequence[mOptionNames.size()]), values, this) .setPositiveButton(android.R.string.ok, this).show(); } /** * "OK" button in dialog was clicked */ @Override public void onClick(DialogInterface dialog, int which) { if (mFlags == mNewFlags) { return; } mFlags = mNewFlags; // Restart scan (new ListFiltersTask()).execute(); } /** * Filtering option was checked or unchecked. * * We apply new flags when user clicks OK */ @Override public void onClick(DialogInterface dialog, int which, boolean isChecked) { int flagValue = mOptionFlags.get(which); if (isChecked) { mNewFlags |= flagValue; } else { mNewFlags &= ~flagValue; } } } private interface ListItem { PackageItemInfo getComponentForDisplay(); String getComponentName(); void handleClick(); } /** * Structure containing application or component with matched filters */ private class AppWithMatchingFilters implements ListItem { final ComponentWithMatchingFilters[] matchingComponents; AppWithMatchingFilters(List<ComponentWithMatchingFilters> matchingComponents) { this.matchingComponents = matchingComponents .toArray(new ComponentWithMatchingFilters[matchingComponents.size()]); } @Override public PackageItemInfo getComponentForDisplay() { return matchingComponents[0].componentInfo.applicationInfo; } @Override public String getComponentName() { return matchingComponents[0].componentInfo.packageName; } @Override public void handleClick() { mAllAppsListViewState = mListView.onSaveInstanceState(); mInPackage = this; mAppListAdapter.notifyDataSetChanged(); } } private class ComponentWithMatchingFilters implements ListItem { final ComponentInfo componentInfo; ComponentWithMatchingFilters(ComponentInfo componentInfo) { this.componentInfo = componentInfo; } @Override public PackageItemInfo getComponentForDisplay() { return componentInfo; } @Override public String getComponentName() { return new ComponentName(componentInfo.packageName, componentInfo.name).flattenToShortString(); } @Override public void handleClick() { final IntentEditorActivity intentEditorActivity = (IntentEditorActivity) getActivity(); intentEditorActivity.setComponentName(new ComponentName(componentInfo.packageName, componentInfo.name)); dismiss(); } } private ArrayList<AppWithMatchingFilters> mAppsWithMatchingFilters = new ArrayList<AppWithMatchingFilters>(); /** * Adapter for list. * Automatically gets values, just call {@link #notifyDataSetChanged()}. */ private class AppListAdapter extends BaseAdapter { @Override public int getCount() { if (!mIsScanningFinished) { return 0; } if (mInPackage != null) { return mInPackage.matchingComponents.length; } return mAppsWithMatchingFilters.size(); } @Override public ListItem getItem(int position) { // Displaying just package items? if (mInPackage != null) { return mInPackage.matchingComponents[position]; } // Get an app AppWithMatchingFilters app = mAppsWithMatchingFilters.get(position); // But if it has only one matching component display it directly if (app.matchingComponents.length == 1) { return app.matchingComponents[0]; } return app; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { // Get app info ListItem item = getItem(position); // Create view if needed if (convertView == null) { convertView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.simple_list_item_2_with_icon, parent, false); } // Get package manager PackageManager pm = parent.getContext().getPackageManager(); // Set texts PackageItemInfo componentForDisplay = item.getComponentForDisplay(); // Title (label) final CharSequence applicationLabel = componentForDisplay.loadLabel(pm); ((TextView) convertView.findViewById(android.R.id.text1)).setText(applicationLabel); // Package/component name (secondary text) ((TextView) convertView.findViewById(android.R.id.text2)).setText(item.getComponentName()); // Icon ((ImageView) convertView.findViewById(R.id.app_icon)) .setImageDrawable(componentForDisplay.loadIcon(pm)); // Return view return convertView; } } /** * Task scanning components in apps and matches intent filters. * * To start or restart use following code: * (new ListFiltersTask()).execute(); */ private class ListFiltersTask implements ExtendedPackageInfo.AllCallback { private int mComponentType; private String mAction; private Set<String> mCategories; boolean mInsensitiveAndSubstring; public void execute() { // Flag us as running and clear list mIsScanningFinished = false; // Get filtering flags int usedFlags = mFlags & mAvailableFlags; mInsensitiveAndSubstring = (usedFlags & FLAG_CASE_INSENSITIVE_AND_SUBSTRING) != 0; // Get requested component type final IntentEditorActivity intentEditor = (IntentEditorActivity) getActivity(); mComponentType = intentEditor.getComponentType(); // Set filtering constraints if ((usedFlags & FLAG_TEST_ACTION) != 0) { mAction = mIntent.getAction(); if (mAction != null && "".equals(mAction)) { mAction = null; } } if ((usedFlags & FLAG_TEST_CATEGORIES) != 0) { if (mInsensitiveAndSubstring) { mCategories = new HashSet<String>(); for (String category : mIntent.getCategories()) { if (mCategories != null) { mCategories.add(category.toLowerCase()); } } } else { mCategories = mIntent.getCategories(); } } // Prepare package info ExtendedPackageInfo.getAllPackageInfos(getActivity(), this); } @Override public void onAllPackagesInfosAvailable(ExtendedPackageInfo[] infos) { mAppsWithMatchingFilters.clear(); // Temporary list holding currently scanned intent filters ArrayList<ComponentWithMatchingFilters> matchingComponents = new ArrayList<ComponentWithMatchingFilters>(); // Iterate through packages for (ExtendedPackageInfo extendedPackageInfo : infos) { // Scan intent filters in package scanIntentFiltersInPackage(extendedPackageInfo, matchingComponents); // Add IntentFilters to list if (matchingComponents.size() != 0) { // Add app to list and refresh mAppsWithMatchingFilters.add(new AppWithMatchingFilters(matchingComponents)); matchingComponents.clear(); // Clear list so we can use it again for next app } } // Set flags that we're finished mIsScanningFinished = true; // Refresh list and/or remove scanning indicator hideProgressAndShowList(); mAppListAdapter.notifyDataSetChanged(); } private void scanIntentFiltersInPackage(ExtendedPackageInfo extendedPackageInfo, ArrayList<ComponentWithMatchingFilters> matchingComponents) { // Get components list and skip app if it's null final ExtendedPackageInfo.ExtendedComponentInfo[] components = extendedPackageInfo .getComponentsByType(mComponentType); if (components == null) { return; } // Iterate over components for (ExtendedPackageInfo.ExtendedComponentInfo component : components) { IntentFilter[] intentFilters = component.intentFilters; if (intentFilters != null) { // Test the intent filters for (IntentFilter intentFilter : intentFilters) { if (testIntentFilter(intentFilter)) { // Component matched, add to list matchingComponents.add(new ComponentWithMatchingFilters(component.systemComponentInfo)); // End scanning this component, scan next in package break; } } } } } private boolean testIntentFilter(IntentFilter intentFilter) { // Check if IntentFilter is valid if (intentFilter.countActions() == 0) { return false; } // Perform tests if (mInsensitiveAndSubstring) { // Lax tests // Lax action test if (mAction != null) { boolean foundAction = false; for (final Iterator<String> actionsIterator = intentFilter.actionsIterator(); actionsIterator .hasNext();) { if (actionsIterator.next().toLowerCase().contains(mAction)) { foundAction = true; break; } } if (!foundAction) { return false; } } // Lax category test if (mCategories != null) { if (intentFilter.countCategories() == 0) { return false; // No categories but we require some } String[] categories = mCategories.toArray(new String[mCategories.size()]); boolean[] foundCategories = new boolean[categories.length]; for (final Iterator<String> categoriesIterator = intentFilter .categoriesIterator(); categoriesIterator.hasNext();) { final String filterCategory = categoriesIterator.next().toLowerCase(); for (int i = 0, categoriesLength = categories.length; i < categoriesLength; i++) { String category = categories[i]; if (filterCategory.contains(category)) { foundCategories[i] = true; } } } for (boolean foundCategory : foundCategories) { if (!foundCategory) { return false; } } } } else { // Strict tests if (mAction != null && !intentFilter.hasAction(mAction)) { return false; } if (intentFilter.matchCategories(mCategories) != null) { return false; } } // If we reached this point IntentFilter matches filtering criteria return true; } } }