package de.robv.android.xposed.installer; import android.Manifest; import android.app.ListFragment; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.support.annotation.NonNull; import android.support.v13.app.FragmentCompat; import android.support.v4.app.ActivityCompat; import android.support.v7.app.ActionBar; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.text.Collator; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import de.robv.android.xposed.installer.installation.StatusInstallerFragment; import de.robv.android.xposed.installer.repo.Module; import de.robv.android.xposed.installer.repo.ModuleVersion; import de.robv.android.xposed.installer.repo.ReleaseType; import de.robv.android.xposed.installer.repo.RepoDb; import de.robv.android.xposed.installer.repo.RepoDb.RowNotFoundException; import de.robv.android.xposed.installer.util.DownloadsUtil; import de.robv.android.xposed.installer.util.ModuleUtil; import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; import de.robv.android.xposed.installer.util.ModuleUtil.ModuleListener; import de.robv.android.xposed.installer.util.NavUtil; import de.robv.android.xposed.installer.util.RepoLoader; import de.robv.android.xposed.installer.util.ThemeUtil; import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS; import static de.robv.android.xposed.installer.XposedApp.WRITE_EXTERNAL_PERMISSION; public class ModulesFragment extends ListFragment implements ModuleListener { public static final String SETTINGS_CATEGORY = "de.robv.android.xposed.category.MODULE_SETTINGS"; public static final String PLAY_STORE_PACKAGE = "com.android.vending"; public static final String PLAY_STORE_LINK = "https://play.google.com/store/apps/details?id=%s"; public static final String XPOSED_REPO_LINK = "http://repo.xposed.info/module/%s"; private static final String NOT_ACTIVE_NOTE_TAG = "NOT_ACTIVE_NOTE"; private static String PLAY_STORE_LABEL = null; private int installedXposedVersion; private ModuleUtil mModuleUtil; private ModuleAdapter mAdapter = null; private PackageManager mPm = null; private Runnable reloadModules = new Runnable() { public void run() { mAdapter.setNotifyOnChange(false); mAdapter.clear(); mAdapter.addAll(mModuleUtil.getModules().values()); final Collator col = Collator.getInstance(Locale.getDefault()); mAdapter.sort(new Comparator<InstalledModule>() { @Override public int compare(InstalledModule lhs, InstalledModule rhs) { return col.compare(lhs.getAppName(), rhs.getAppName()); } }); mAdapter.notifyDataSetChanged(); } }; private MenuItem mClickedMenuItem = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mModuleUtil = ModuleUtil.getInstance(); mPm = getActivity().getPackageManager(); if (PLAY_STORE_LABEL == null) { try { ApplicationInfo ai = mPm.getApplicationInfo(PLAY_STORE_PACKAGE, 0); PLAY_STORE_LABEL = mPm.getApplicationLabel(ai).toString(); } catch (NameNotFoundException ignored) { } } } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); installedXposedVersion = XposedApp.getInstalledXposedVersion(); if (installedXposedVersion < 0 || XposedApp.getActiveXposedVersion() < 0 || StatusInstallerFragment.DISABLE_FILE.exists()) { View notActiveNote = getActivity().getLayoutInflater().inflate(R.layout.xposed_not_active_note, getListView(), false); if (installedXposedVersion < 0) { ((TextView) notActiveNote.findViewById(android.R.id.title)).setText(R.string.framework_not_installed); } notActiveNote.setTag(NOT_ACTIVE_NOTE_TAG); getListView().addHeaderView(notActiveNote); } mAdapter = new ModuleAdapter(getActivity()); reloadModules.run(); setListAdapter(mAdapter); setEmptyText(getActivity().getString(R.string.no_xposed_modules_found)); registerForContextMenu(getListView()); mModuleUtil.addListener(this); ActionBar actionBar = ((WelcomeActivity) getActivity()).getSupportActionBar(); DisplayMetrics metrics = getResources().getDisplayMetrics(); int sixDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6, metrics); int eightDp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, metrics); assert actionBar != null; int toolBarDp = actionBar.getHeight() == 0 ? 196 : actionBar.getHeight(); getListView().setDivider(null); getListView().setDividerHeight(sixDp); getListView().setPadding(eightDp, toolBarDp + eightDp, eightDp, eightDp); getListView().setClipToPadding(false); setHasOptionsMenu(true); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { // TODO maybe enable again after checking the implementation //inflater.inflate(R.menu.menu_modules, menu); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == WRITE_EXTERNAL_PERMISSION) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (mClickedMenuItem != null) { new Handler().postDelayed(new Runnable() { @Override public void run() { onOptionsItemSelected(mClickedMenuItem); } }, 500); } } else { Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); } } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.bookmarks) { startActivity(new Intent(getActivity(), ModulesBookmark.class)); return true; } String backupPath = Environment.getExternalStorageDirectory() + "/XposedInstaller"; File enabledModulesPath = new File(backupPath, "enabled_modules.list"); File installedModulesPath = new File(backupPath, "installed_modules.list"); File targetDir = new File(backupPath); File listModules = new File(XposedApp.ENABLED_MODULES_LIST_FILE); mClickedMenuItem = item; if (checkPermissions()) return false; switch (item.getItemId()) { case R.id.export_enabled_modules: if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { return false; } if (ModuleUtil.getInstance().getEnabledModules().isEmpty()) { Toast.makeText(getActivity(), getString(R.string.no_enabled_modules), Toast.LENGTH_SHORT).show(); return false; } try { if (!targetDir.exists()) targetDir.mkdir(); FileInputStream in = new FileInputStream(listModules); FileOutputStream out = new FileOutputStream(enabledModulesPath); byte[] buffer = new byte[1024]; int len; while ((len = in.read(buffer)) > 0) { out.write(buffer, 0, len); } in.close(); out.close(); } catch (IOException e) { Toast.makeText(getActivity(), getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Toast.LENGTH_LONG).show(); return false; } Toast.makeText(getActivity(), enabledModulesPath.toString(), Toast.LENGTH_LONG).show(); return true; case R.id.export_installed_modules: if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { Toast.makeText(getActivity(), R.string.sdcard_not_writable, Toast.LENGTH_LONG).show(); return false; } Map<String, InstalledModule> installedModules = ModuleUtil.getInstance().getModules(); if (installedModules.isEmpty()) { Toast.makeText(getActivity(), getString(R.string.no_installed_modules), Toast.LENGTH_SHORT).show(); return false; } try { if (!targetDir.exists()) targetDir.mkdir(); FileWriter fw = new FileWriter(installedModulesPath); BufferedWriter bw = new BufferedWriter(fw); PrintWriter fileOut = new PrintWriter(bw); Set keys = installedModules.keySet(); for (Object key1 : keys) { String packageName = (String) key1; fileOut.println(packageName); } fileOut.close(); } catch (IOException e) { Toast.makeText(getActivity(), getResources().getString(R.string.logs_save_failed) + "n" + e.getMessage(), Toast.LENGTH_LONG).show(); return false; } Toast.makeText(getActivity(), installedModulesPath.toString(), Toast.LENGTH_LONG).show(); return true; case R.id.import_installed_modules: return importModules(installedModulesPath); case R.id.import_enabled_modules: return importModules(enabledModulesPath); } return super.onOptionsItemSelected(item); } private boolean checkPermissions() { if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { FragmentCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION); return true; } return false; } private boolean importModules(File path) { if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { Toast.makeText(getActivity(), R.string.sdcard_not_writable, Toast.LENGTH_LONG).show(); return false; } InputStream ips = null; RepoLoader repoLoader = RepoLoader.getInstance(); List<Module> list = new ArrayList<>(); if (!path.exists()) { Toast.makeText(getActivity(), getString(R.string.no_backup_found), Toast.LENGTH_LONG).show(); return false; } try { ips = new FileInputStream(path); } catch (FileNotFoundException e) { Log.e(XposedApp.TAG, "Could not open " + path, e); } if (path.length() == 0) { Toast.makeText(getActivity(), R.string.file_is_empty, Toast.LENGTH_LONG).show(); return false; } try { assert ips != null; InputStreamReader ipsr = new InputStreamReader(ips); BufferedReader br = new BufferedReader(ipsr); String line; while ((line = br.readLine()) != null) { Module m = repoLoader.getModule(line); if (m == null) { Toast.makeText(getActivity(), getString(R.string.download_details_not_found, line), Toast.LENGTH_SHORT).show(); } else { list.add(m); } } br.close(); } catch (ActivityNotFoundException | IOException e) { Toast.makeText(getActivity(), e.toString(), Toast.LENGTH_SHORT).show(); } for (Module m : list) { ModuleVersion mv = null; for (int i = 0; i < m.versions.size(); i++) { ModuleVersion mvTemp = m.versions.get(i); if (mvTemp.relType == ReleaseType.STABLE) { mv = mvTemp; break; } } if (mv != null) { DownloadsUtil.add(getActivity(), m.name, mv.downloadLink, new DownloadsUtil.DownloadFinishedCallback() { @Override public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) { XposedApp.installApk(context, info); } }, DownloadsUtil.MIME_TYPES.APK); } } return true; } @Override public void onDestroyView() { super.onDestroyView(); mModuleUtil.removeListener(this); setListAdapter(null); mAdapter = null; } @Override public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { getActivity().runOnUiThread(reloadModules); } @Override public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { getActivity().runOnUiThread(reloadModules); } @Override public void onListItemClick(ListView l, View v, int position, long id) { String packageName = (String) v.getTag(); if (packageName == null) return; if (packageName.equals(NOT_ACTIVE_NOTE_TAG)) { ((WelcomeActivity) getActivity()).switchFragment(0); return; } Intent launchIntent = getSettingsIntent(packageName); if (launchIntent != null) startActivity(launchIntent); else Toast.makeText(getActivity(), getActivity().getString(R.string.module_no_ui), Toast.LENGTH_LONG).show(); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { InstalledModule installedModule = getItemFromContextMenuInfo(menuInfo); if (installedModule == null) return; menu.setHeaderTitle(installedModule.getAppName()); getActivity().getMenuInflater().inflate(R.menu.context_menu_modules, menu); if (getSettingsIntent(installedModule.packageName) == null) menu.removeItem(R.id.menu_launch); try { String support = RepoDb .getModuleSupport(installedModule.packageName); if (NavUtil.parseURL(support) == null) menu.removeItem(R.id.menu_support); } catch (RowNotFoundException e) { menu.removeItem(R.id.menu_download_updates); menu.removeItem(R.id.menu_support); } String installer = mPm.getInstallerPackageName(installedModule.packageName); if (PLAY_STORE_LABEL != null && PLAY_STORE_PACKAGE.equals(installer)) menu.findItem(R.id.menu_play_store).setTitle(PLAY_STORE_LABEL); else menu.removeItem(R.id.menu_play_store); } @Override public boolean onContextItemSelected(MenuItem item) { InstalledModule module = getItemFromContextMenuInfo(item.getMenuInfo()); if (module == null) return false; switch (item.getItemId()) { case R.id.menu_launch: startActivity(getSettingsIntent(module.packageName)); return true; case R.id.menu_download_updates: Intent detailsIntent = new Intent(getActivity(), DownloadDetailsActivity.class); detailsIntent.setData(Uri.fromParts("package", module.packageName, null)); startActivity(detailsIntent); return true; case R.id.menu_support: NavUtil.startURL(getActivity(), Uri.parse(RepoDb.getModuleSupport(module.packageName))); return true; case R.id.menu_play_store: Intent i = new Intent(android.content.Intent.ACTION_VIEW); i.setData(Uri.parse(String.format(PLAY_STORE_LINK, module.packageName))); i.setPackage(PLAY_STORE_PACKAGE); try { startActivity(i); } catch (ActivityNotFoundException e) { i.setPackage(null); startActivity(i); } return true; case R.id.menu_app_info: startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", module.packageName, null))); return true; case R.id.menu_uninstall: startActivity(new Intent(Intent.ACTION_UNINSTALL_PACKAGE, Uri.fromParts("package", module.packageName, null))); return true; } return false; } private InstalledModule getItemFromContextMenuInfo(ContextMenuInfo menuInfo) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; int position = info.position - getListView().getHeaderViewsCount(); return (position >= 0) ? (InstalledModule) getListAdapter().getItem(position) : null; } private Intent getSettingsIntent(String packageName) { // taken from // ApplicationPackageManager.getLaunchIntentForPackage(String) // first looks for an Xposed-specific category, falls back to // getLaunchIntentForPackage PackageManager pm = getActivity().getPackageManager(); Intent intentToResolve = new Intent(Intent.ACTION_MAIN); intentToResolve.addCategory(SETTINGS_CATEGORY); intentToResolve.setPackage(packageName); List<ResolveInfo> ris = pm.queryIntentActivities(intentToResolve, 0); if (ris == null || ris.size() <= 0) { return pm.getLaunchIntentForPackage(packageName); } Intent intent = new Intent(intentToResolve); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setClassName(ris.get(0).activityInfo.packageName, ris.get(0).activityInfo.name); return intent; } private class ModuleAdapter extends ArrayAdapter<InstalledModule> { public ModuleAdapter(Context context) { super(context, R.layout.list_item_module, R.id.title); } @Override public View getView(int position, View convertView, ViewGroup parent) { View view = super.getView(position, convertView, parent); if (convertView == null) { // The reusable view was created for the first time, set up the // listener on the checkbox ((CheckBox) view.findViewById(R.id.checkbox)).setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { String packageName = (String) buttonView.getTag(); boolean changed = mModuleUtil.isModuleEnabled(packageName) ^ isChecked; if (changed) { mModuleUtil.setModuleEnabled(packageName, isChecked); mModuleUtil.updateModulesList(true); } } }); } InstalledModule item = getItem(position); TextView version = (TextView) view.findViewById(R.id.version_name); version.setText(item.versionName); // Store the package name in some views' tag for later access view.findViewById(R.id.checkbox).setTag(item.packageName); view.setTag(item.packageName); ((ImageView) view.findViewById(R.id.icon)).setImageDrawable(item.getIcon()); TextView descriptionText = (TextView) view.findViewById(R.id.description); if (!item.getDescription().isEmpty()) { descriptionText.setText(item.getDescription()); descriptionText.setTextColor(ThemeUtil.getThemeColor(getContext(), android.R.attr.textColorSecondary)); } else { descriptionText.setText(getString(R.string.module_empty_description)); descriptionText.setTextColor(getResources().getColor(R.color.warning)); } CheckBox checkbox = (CheckBox) view.findViewById(R.id.checkbox); checkbox.setChecked(mModuleUtil.isModuleEnabled(item.packageName)); TextView warningText = (TextView) view.findViewById(R.id.warning); if (item.minVersion == 0) { checkbox.setEnabled(false); warningText.setText(getString(R.string.no_min_version_specified)); warningText.setVisibility(View.VISIBLE); } else if (installedXposedVersion != 0 && item.minVersion > installedXposedVersion) { checkbox.setEnabled(false); warningText.setText(String.format(getString(R.string.warning_xposed_min_version), item.minVersion)); warningText.setVisibility(View.VISIBLE); } else if (item.minVersion < ModuleUtil.MIN_MODULE_VERSION) { checkbox.setEnabled(false); warningText.setText(String.format(getString(R.string.warning_min_version_too_low), item.minVersion, ModuleUtil.MIN_MODULE_VERSION)); warningText.setVisibility(View.VISIBLE); } else if (item.isInstalledOnExternalStorage()) { checkbox.setEnabled(false); warningText.setText(getString(R.string.warning_installed_on_external_storage)); warningText.setVisibility(View.VISIBLE); } else if (installedXposedVersion == 0) { checkbox.setEnabled(false); warningText.setText(getString(R.string.framework_not_installed)); warningText.setVisibility(View.VISIBLE); } else { checkbox.setEnabled(true); warningText.setVisibility(View.GONE); } return view; } } }