/* * Copyright (C) 2013 - 2015 Alexander "Evisceration" Martinz * * 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 org.namelessrom.devicecontrol.modules.appmanager; import android.animation.Animator; import android.animation.ObjectAnimator; import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.DialogInterface; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v7.widget.LinearLayoutManager; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.SearchView; import android.widget.TextView; import org.namelessrom.devicecontrol.App; import org.namelessrom.devicecontrol.R; import org.namelessrom.devicecontrol.utils.SortHelper; import org.namelessrom.devicecontrol.views.CustomRecyclerView; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import at.amartinz.execution.NormalShell; import hugo.weaving.DebugLog; import timber.log.Timber; public abstract class BaseAppListFragment extends Fragment implements SearchView.OnQueryTextListener, SearchView.OnCloseListener, View.OnClickListener { private static final int ANIM_DURATION = 450; private AppListAdapter mAdapter; private CustomRecyclerView mRecyclerView; private TextView mEmptyView; private LinearLayout mProgressContainer; private final HashSet<AppItem> mSelectedApps = new HashSet<>(); private HorizontalScrollView mAppListBar; private boolean mIsLoading; public interface AppSelectedListener { void onAppSelected(String packageName, ArrayList<AppItem> selectedApps); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.menu_app_list, menu); // setup search final MenuItem searchItem = menu.findItem(R.id.action_search); final SearchView searchView = (searchItem != null ? (SearchView) searchItem.getActionView() : null); if (searchView != null) { searchView.setOnQueryTextListener(this); searchView.setOnCloseListener(this); } super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onQueryTextSubmit(String s) { return false; } @Override public boolean onQueryTextChange(String s) { if (mAdapter != null) { mAdapter.filter(s); updateVisibility(mAdapter.getItemCount() <= 0); } return true; } @Override public boolean onClose() { if (mAdapter != null) { mAdapter.filter(null); updateVisibility(mAdapter.getItemCount() <= 0); } return false; } @Override public boolean onOptionsItemSelected(final MenuItem item) { final int id = item.getItemId(); if (id == R.id.menu_action_refresh) { loadApps(true); return true; } return false; } @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { final View rootView = inflater.inflate(R.layout.fragment_app_list, container, false); mRecyclerView = (CustomRecyclerView) rootView.findViewById(android.R.id.list); mEmptyView = (TextView) rootView.findViewById(android.R.id.empty); mProgressContainer = (LinearLayout) rootView.findViewById(R.id.progressContainer); mAppListBar = (HorizontalScrollView) rootView.findViewById(R.id.app_bar); rootView.findViewById(R.id.app_bar_uninstall).setOnClickListener(this); rootView.findViewById(R.id.app_bar_enable).setOnClickListener(this); rootView.findViewById(R.id.app_bar_disable).setOnClickListener(this); return rootView; } @Override public void onViewCreated(final View view, final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); setHasOptionsMenu(true); mRecyclerView.setHasFixedSize(true); final LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity()); mRecyclerView.setLayoutManager(linearLayoutManager); } @Override public void onResume() { super.onResume(); loadApps(false); } @Override public void onDestroy() { super.onDestroy(); } private void invalidateOptionsMenu() { if (getActivity() != null) { getActivity().invalidateOptionsMenu(); } } @Override public void onClick(View v) { final int id = v.getId(); switch (id) { case R.id.app_bar_uninstall: case R.id.app_bar_enable: case R.id.app_bar_disable: { showActionDialog(id); break; } } } private void showActionDialog(final int type) { int title; String message; switch (type) { default: case R.id.app_bar_uninstall: { title = R.string.uninstall; message = getString(R.string.uninstall_msg_multi, mSelectedApps.size()); break; } case R.id.app_bar_enable: { title = R.string.enable; if (mSelectedApps.size() > 1) { message = getString(R.string.enable_msg_multi, mSelectedApps.size()); } else { message = getString(R.string.enable_msg_single); } break; } case R.id.app_bar_disable: { title = R.string.disable; if (mSelectedApps.size() > 1) { message = getString(R.string.disable_msg_multi, mSelectedApps.size()); } else { message = getString(R.string.disable_msg_single); } break; } } final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(title); builder.setMessage(message); builder.setNegativeButton(android.R.string.cancel, null); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { showProcessingDialog(type); } }); builder.show(); } private void showProcessingDialog(final int type) { int titleResId; int messageResId; switch (type) { default: case R.id.app_bar_uninstall: { titleResId = R.string.uninstall; messageResId = R.string.uninstall_msg_multi_action; break; } case R.id.app_bar_enable: { titleResId = R.string.enable; messageResId = R.string.enable_msg_multi_action; break; } case R.id.app_bar_disable: { titleResId = R.string.disable; messageResId = R.string.disable_msg_multi_action; break; } } new ProcessTask(getActivity(), type, titleResId, messageResId, mSelectedApps).execute(); } private class ProcessTask extends AsyncTask<Void, Integer, Void> { private final Activity activity; private final int type; private final int messageResId; private final HashSet<AppItem> selectedApps; private final int length; private final ProgressDialog progressDialog; public ProcessTask(Activity activity, int type, int titleResId, int messageResId, HashSet<AppItem> selectedApps) { this.activity = activity; this.type = type; this.messageResId = messageResId; this.selectedApps = selectedApps; this.length = this.selectedApps.size(); progressDialog = new ProgressDialog(this.activity); progressDialog.setTitle(titleResId); progressDialog.setMessage(activity.getString(messageResId, 0, this.length)); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progressDialog.setMax(this.length); progressDialog.setProgress(0); } @Override protected void onPreExecute() { progressDialog.show(); } @Override protected void onProgressUpdate(Integer... values) { // increase current app counter by one as we deliver indexes final int currentApp = values[0] + 1; progressDialog.setMessage(activity.getString(messageResId, currentApp, this.length)); progressDialog.setProgress(currentApp); } @Override protected Void doInBackground(Void... params) { int counter = 0; for (AppItem appItem : selectedApps) { publishProgress(counter); switch (type) { case R.id.app_bar_uninstall: { appItem.uninstall(activity, null, false); break; } case R.id.app_bar_enable: { appItem.enable(null); break; } case R.id.app_bar_disable: { appItem.disable(null); break; } } counter++; } // wait for 750 ms to let everything update its state try { Thread.sleep(750); } catch (Exception ignored) { } return null; } @Override protected void onPostExecute(Void aVoid) { progressDialog.hide(); loadApps(true); Snackbar.make(BaseAppListFragment.this.mAppListBar, R.string.action_completed, Snackbar.LENGTH_LONG).show(); } } public void loadApps(final boolean animate) { if (mIsLoading) { return; } mIsLoading = true; mProgressContainer.post(new Runnable() { @Override public void run() { mProgressContainer.setVisibility(View.VISIBLE); if (animate) { final ObjectAnimator anim = ObjectAnimator.ofFloat(mProgressContainer, "alpha", 0f, 1f); anim.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { new LoadApps().execute(); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); anim.setDuration(ANIM_DURATION); anim.start(); } else { mProgressContainer.setAlpha(1f); new LoadApps().execute(); } } }); } private void updateVisibility(boolean isEmpty) { mRecyclerView.setVisibility(isEmpty ? View.GONE : View.VISIBLE); mEmptyView.setVisibility(isEmpty ? View.VISIBLE : View.GONE); } protected abstract boolean isFiltered(@NonNull ApplicationInfo applicationInfo); private class LoadApps extends AsyncTask<Void, Void, ArrayList<AppItem>> { @Override protected ArrayList<AppItem> doInBackground(Void... params) { final PackageManager pm = App.get().getPackageManager(); final ArrayList<AppItem> appList = new ArrayList<>(); final List<PackageInfo> pkgInfos = new ArrayList<>(); List<PackageInfo> tmp = getInstalledPackages(pm); if (tmp == null || tmp.isEmpty()) { tmp = getInstalledPackagesShell(pm); } pkgInfos.addAll(tmp); for (final PackageInfo pkgInfo : pkgInfos) { if (pkgInfo.applicationInfo == null || isFiltered(pkgInfo.applicationInfo)) { continue; } appList.add(new AppItem(pkgInfo, String.valueOf(pkgInfo.applicationInfo.loadLabel(pm)))); } Collections.sort(appList, SortHelper.sAppComparator); return appList; } @DebugLog @Nullable private List<PackageInfo> getInstalledPackages(PackageManager pm) { try { return pm.getInstalledPackages(0); } catch (Exception exc) { Timber.e(exc, "Could not get installed packages via package manager, falling back..."); } return null; } @DebugLog @NonNull private List<PackageInfo> getInstalledPackagesShell(PackageManager pm) { final List<PackageInfo> pkgInfos = new ArrayList<>(); final List<String> cmdResultList = NormalShell.fireAndBlockList("pm list packages"); if (cmdResultList != null && !cmdResultList.isEmpty()) { for (final String cmdResult : cmdResultList) { if (TextUtils.isEmpty(cmdResult)) { continue; } final String pkgName = cmdResult.substring(cmdResult.indexOf(":") + 1); try { final PackageInfo pkgInfo = pm.getPackageInfo(pkgName, 0); pkgInfos.add(pkgInfo); } catch (Exception ignored) { } } } return pkgInfos; } @Override protected void onPostExecute(final ArrayList<AppItem> appItems) { final ObjectAnimator anim = ObjectAnimator.ofFloat(mProgressContainer, "alpha", 1f, 0f); anim.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { final boolean isEmpty = (appItems == null || appItems.size() <= 0); updateVisibility(isEmpty); } @Override public void onAnimationEnd(Animator animation) { if (appItems != null) { if (mAdapter == null) { final AppListAdapter adapter = new AppListAdapter(getActivity(), BaseAppListFragment.this, appItems, mUninstallListener, mAppSelectedListener); mRecyclerView.setAdapter(adapter); mAdapter = adapter; } else { mAdapter.refill(appItems); } } mProgressContainer.setVisibility(View.GONE); mIsLoading = false; } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); anim.setDuration(ANIM_DURATION); anim.start(); invalidateOptionsMenu(); } } private final AppItem.UninstallListener mUninstallListener = new AppItem.UninstallListener() { @Override public void OnUninstallComplete() { loadApps(true); } }; private final AppSelectedListener mAppSelectedListener = new AppSelectedListener() { @Override public void onAppSelected(String packageName, ArrayList<AppItem> selectedApps) { mSelectedApps.clear(); mSelectedApps.addAll(selectedApps); if (mSelectedApps.size() == 0) { mAppListBar.setVisibility(View.GONE); } else { mAppListBar.setVisibility(View.VISIBLE); } } }; }