diff --git a/app/src/main/assets/suggestions.json b/app/src/main/assets/suggestions.json new file mode 100644 index 00000000000..733a61f5866 --- /dev/null +++ b/app/src/main/assets/suggestions.json @@ -0,0 +1 @@ +[{"id":"org.bromite.bromite","label":"Bromite","source":"f","repo":"https:\/\/github.com\/bromite\/bromite","_id":"browsers"},{"id":"org.chromium.chrome","label":"Vanadium","repo":"https:\/\/github.com\/GrapheneOS\/Vanadium","reason":"GrapheneOS only","_id":"browsers"},{"id":"itkach.aard2","label":"Aard 2","reason":"Simple yet powerful dictionary reader that support multiple dictionaries with offline access.\nSLOB files https:\/\/github.com\/itkach\/slob\/wiki\/Dictionaries","source":"fg","repo":"https:\/\/github.com\/itkach\/aard2-android","_id":"dictionaries"},{"id":"eu.faircode.email","label":"FairEmail","source":"fg","repo":"https:\/\/github.com\/M66B\/FairEmail","_id":"email_clients"},{"id":"com.fsck.k9","label":"K-9 Mail","source":"fg","repo":"https:\/\/github.com\/thundernest\/k-9","_id":"email_clients"},{"id":"me.zhanghai.android.files","label":"Material Files","source":"fg","repo":"https:\/\/github.com\/zhanghai\/MaterialFiles","_id":"file_managers"},{"id":"deckers.thibault.aves.libre","label":"Aves Libre","source":"f","repo":"https:\/\/github.com\/deckerst\/aves","_id":"gallery"},{"id":"com.simplemobiletools.gallery.pro","label":"Simple Gallery Pro","source":"f","repo":"https:\/\/github.com\/SimpleMobileTools\/Simple-Gallery","_id":"gallery"},{"id":"org.smc.inputmethod.indic","label":"Indic Keyboard","source":"fg","repo":"https:\/\/gitlab.com\/indicproject\/Indic-Keyboard","_id":"keyboards"},{"id":"dev.patrickgold.florisboard","label":"FlorisBoard","source":"f","repo":"https:\/\/github.com\/florisboard\/florisboard","_id":"keyboards"},{"id":"com.x8bit.bitwarden","label":"Bitwarden","reason":"Regular security audits https:\/\/bitwarden.com\/help\/is-bitwarden-audited\/#third-party-security-audits.\nRecommended to download from their official F-Droid repo https:\/\/mobileapp.bitwarden.com\/fdroid\/.","source":"g","repo":"https:\/\/github.com\/bitwarden\/mobile","_id":"password_managers"},{"id":"com.moez.QKSMS","label":"QKSMS","source":"fg","repo":"https:\/\/github.com\/moezbhatti\/qksms","_id":"sms"},{"id":"org.bromite.webview","label":"Bromite SystemWebView","source":"f","repo":"https:\/\/github.com\/bromite\/bromite","_id":"webviews"}] \ No newline at end of file diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/debloat/BloatwareDetailsDialog.java b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/BloatwareDetailsDialog.java new file mode 100644 index 00000000000..62f06de822a --- /dev/null +++ b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/BloatwareDetailsDialog.java @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package io.github.muntashirakon.AppManager.debloat; + +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.SpannableStringBuilder; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.widget.LinearLayoutCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.chip.Chip; +import com.google.android.material.textview.MaterialTextView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import io.github.muntashirakon.AppManager.R; +import io.github.muntashirakon.AppManager.details.AppDetailsActivity; +import io.github.muntashirakon.AppManager.utils.UIUtils; +import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; +import io.github.muntashirakon.dialog.CapsuleBottomSheetDialogFragment; +import io.github.muntashirakon.util.UiUtils; +import io.github.muntashirakon.widget.FlowLayout; +import io.github.muntashirakon.widget.MaterialAlertView; +import io.github.muntashirakon.widget.RecyclerView; + + +public class BloatwareDetailsDialog extends CapsuleBottomSheetDialogFragment { + public static final String TAG = BloatwareDetailsDialog.class.getSimpleName(); + + public static final String ARG_PACKAGE_NAME = "pkg"; + + @NonNull + public static BloatwareDetailsDialog getInstance(@NonNull String packageName) { + BloatwareDetailsDialog fragment = new BloatwareDetailsDialog(); + Bundle args = new Bundle(); + args.putString(ARG_PACKAGE_NAME, packageName); + fragment.setArguments(args); + return fragment; + } + + private ImageView mAppIconView; + private MaterialButton mOpenAppInfoButton; + private TextView mAppLabelView; + private TextView mPackageNameView; + private FlowLayout mFlowLayout; + private MaterialAlertView mWarningView; + private MaterialTextView mDescriptionView; + private LinearLayoutCompat mSuggestionContainer; + private RecyclerView mSuggestionView; + private SuggestionsAdapter mAdapter; + + + @NonNull + @Override + public View initRootView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.dialog_bloatware_details, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + String packageName = requireArguments().getString(ARG_PACKAGE_NAME); + if (packageName == null) { + dismiss(); + return; + } + DebloaterViewModel viewModel = new ViewModelProvider(requireActivity()).get(DebloaterViewModel.class); + mAppIconView = view.findViewById(R.id.icon); + mOpenAppInfoButton = view.findViewById(R.id.info); + mAppLabelView = view.findViewById(R.id.name); + mPackageNameView = view.findViewById(R.id.package_name); + mFlowLayout = view.findViewById(R.id.tag_cloud); + mWarningView = view.findViewById(R.id.alert_text); + mDescriptionView = view.findViewById(R.id.apk_description); + mSuggestionContainer = view.findViewById(R.id.container); + mSuggestionView = view.findViewById(R.id.recycler_view); + mSuggestionView.setLayoutManager(new LinearLayoutManager(requireContext())); + mAdapter = new SuggestionsAdapter(); + mSuggestionView.setAdapter(mAdapter); + + viewModel.getDebloatObjectLiveData().observe(getViewLifecycleOwner(), debloatObject -> { + if (debloatObject == null) { + dismiss(); + return; + } + updateDialog(debloatObject); + updateDialog(debloatObject.getSuggestions()); + }); + viewModel.findDebloatObject(packageName); + } + + private void updateDialog(@NonNull DebloatObject debloatObject) { + Drawable icon = debloatObject.getIcon(); + mAppIconView.setImageDrawable(icon != null ? icon : requireActivity().getPackageManager().getDefaultActivityIcon()); + int[] users = debloatObject.getUsers(); + if (users != null && users.length > 0) { + mOpenAppInfoButton.setOnClickListener(v -> { + Intent appDetailsIntent = AppDetailsActivity.getIntent(requireContext(), debloatObject.packageName, + users[0]); + startActivity(appDetailsIntent); + dismiss(); + }); + } else { + mOpenAppInfoButton.setVisibility(View.GONE); + } + CharSequence label = debloatObject.getLabel(); + mAppLabelView.setText(label != null ? label : debloatObject.packageName); + mPackageNameView.setText(debloatObject.packageName); + String warning = debloatObject.getWarning(); + if (warning != null) { + mWarningView.setText(warning); + if (debloatObject.getRemoval() != DebloatObject.REMOVAL_CAUTION) { + mWarningView.setAlertType(MaterialAlertView.ALERT_TYPE_INFO); + } + } else mWarningView.setVisibility(View.GONE); + mDescriptionView.setText(getDescription(debloatObject.getDescription(), debloatObject.getWebRefs())); + // Add tags + int removalColor; + @StringRes + int removalRes; + switch (debloatObject.getRemoval()) { + case DebloatObject.REMOVAL_SAFE: + removalColor = ColorCodes.getRemovalSafeIndicatorColor(requireContext()); + removalRes = R.string.debloat_removal_safe_short_description; + break; + default: + case DebloatObject.REMOVAL_CAUTION: + removalColor = ColorCodes.getRemovalCautionIndicatorColor(requireContext()); + removalRes = R.string.debloat_removal_caution_short_description; + break; + case DebloatObject.REMOVAL_REPLACE: + removalColor = ColorCodes.getRemovalReplaceIndicatorColor(requireContext()); + removalRes = R.string.debloat_removal_replace_short_description; + break; + } + mFlowLayout.removeAllViews(); + addTag(mFlowLayout, debloatObject.type); + addTag(mFlowLayout, removalRes, removalColor); + } + + private void updateDialog(@Nullable List suggestionObjects) { + if (suggestionObjects == null || suggestionObjects.isEmpty()) { + mSuggestionContainer.setVisibility(View.GONE); + return; + } + mSuggestionContainer.setVisibility(View.VISIBLE); + mAdapter.setList(suggestionObjects); + } + + @NonNull + private CharSequence getDescription(@NonNull String description, @Nullable String[] refSites) { + SpannableStringBuilder sb = new SpannableStringBuilder(); + sb.append(description.trim()); + if (refSites == null || refSites.length == 0) { + return sb; + } + // Add references + return sb.append(UIUtils.getBoldString("\n\nReferences\n")) + .append(UiUtils.getOrderedList(Arrays.asList(refSites))); + } + + private void addTag(@NonNull ViewGroup parent, @StringRes int titleRes, @ColorInt int textColor) { + Chip chip = (Chip) LayoutInflater.from(requireContext()).inflate(R.layout.item_chip, parent, false); + chip.setText(titleRes); + if (textColor >= 0) { + chip.setTextColor(textColor); + } + parent.addView(chip); + } + + private void addTag(@NonNull ViewGroup parent, @NonNull CharSequence title) { + Chip chip = (Chip) LayoutInflater.from(requireContext()).inflate(R.layout.item_chip, parent, false); + chip.setText(title); + parent.addView(chip); + } + + private class SuggestionsAdapter extends RecyclerView.Adapter { + private final List mSuggestions = Collections.synchronizedList(new ArrayList<>()); + + public SuggestionsAdapter() { + } + + public void setList(@NonNull List suggestions) { + mSuggestions.clear(); + mSuggestions.addAll(suggestions); + notifyDataSetChanged(); + } + + @NonNull + @Override + public SuggestionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_bloatware_details, parent, false); + return new SuggestionViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull SuggestionViewHolder holder, int position) { + SuggestionObject suggestion = mSuggestions.get(position); + holder.labelView.setText(suggestion.getLabel()); + holder.packageNameView.setText(suggestion.packageName); + int[] users = suggestion.getUsers(); + if (users != null && users.length > 0) { + MaterialButton appInfoButton = holder.marketOrAppInfoButton; + appInfoButton.setIconResource(io.github.muntashirakon.ui.R.drawable.ic_information); + appInfoButton.setOnClickListener(v -> { + Intent appDetailsIntent = AppDetailsActivity.getIntent(requireContext(), suggestion.packageName, + users[0]); + startActivity(appDetailsIntent); + }); + } else { + MaterialButton marketButton = holder.marketOrAppInfoButton; + marketButton.setIconResource(suggestion.isInFDroidMarket() ? R.drawable.ic_frost_fdroid : R.drawable.ic_frost_aurorastore); + marketButton.setOnClickListener(v -> { + Intent appDetailsIntent = suggestion.getMarketLink(); + appDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + startActivity(appDetailsIntent); + } catch (Throwable th) { + UIUtils.displayLongToast("Error: " + th.getMessage()); + } + }); + } + String reason = suggestion.getReason(); + StringBuilder sb = new StringBuilder(); + if (reason != null) sb.append(reason).append("\n"); + sb.append(suggestion.getRepo()); + holder.repoView.setText(sb); + } + + @Override + public int getItemCount() { + return mSuggestions.size(); + } + + @Override + public long getItemId(int position) { + return mSuggestions.get(position).hashCode(); + } + + private class SuggestionViewHolder extends RecyclerView.ViewHolder { + final TextView labelView; + final TextView packageNameView; + final TextView repoView; + final MaterialButton marketOrAppInfoButton; + + public SuggestionViewHolder(@NonNull View itemView) { + super(itemView); + labelView = itemView.findViewById(R.id.name); + packageNameView = itemView.findViewById(R.id.package_name); + repoView = itemView.findViewById(R.id.message); + marketOrAppInfoButton = itemView.findViewById(R.id.info); + } + } + } +} diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloatItemDetailsDialog.java b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloatItemDetailsDialog.java deleted file mode 100644 index 466e13c5526..00000000000 --- a/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloatItemDetailsDialog.java +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package io.github.muntashirakon.AppManager.debloat; - -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.text.SpannableStringBuilder; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.lifecycle.ViewModelProvider; - -import com.google.android.material.button.MaterialButton; -import com.google.android.material.chip.Chip; -import com.google.android.material.textview.MaterialTextView; - -import java.util.Arrays; - -import io.github.muntashirakon.AppManager.R; -import io.github.muntashirakon.AppManager.details.AppDetailsActivity; -import io.github.muntashirakon.AppManager.utils.UIUtils; -import io.github.muntashirakon.AppManager.utils.appearance.ColorCodes; -import io.github.muntashirakon.dialog.CapsuleBottomSheetDialogFragment; -import io.github.muntashirakon.util.UiUtils; -import io.github.muntashirakon.widget.FlowLayout; -import io.github.muntashirakon.widget.MaterialAlertView; - - -public class DebloatItemDetailsDialog extends CapsuleBottomSheetDialogFragment { - public static final String TAG = DebloatItemDetailsDialog.class.getSimpleName(); - - public static final String ARG_PACKAGE_NAME = "pkg"; - - @NonNull - public static DebloatItemDetailsDialog getInstance(@NonNull String packageName) { - DebloatItemDetailsDialog fragment = new DebloatItemDetailsDialog(); - Bundle args = new Bundle(); - args.putString(ARG_PACKAGE_NAME, packageName); - fragment.setArguments(args); - return fragment; - } - - @NonNull - @Override - public View initRootView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.dialog_debloat_item_details, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - String packageName = requireArguments().getString(ARG_PACKAGE_NAME); - if (packageName == null) { - dismiss(); - return; - } - DebloaterViewModel viewModel = new ViewModelProvider(requireActivity()).get(DebloaterViewModel.class); - DebloatObject debloatObject = viewModel.findDebloatObject(packageName); - if (debloatObject == null) { - dismiss(); - return; - } - ImageView appIconView = view.findViewById(R.id.icon); - MaterialButton openAppInfoButton = view.findViewById(R.id.info); - TextView appLabelView = view.findViewById(R.id.name); - TextView packageNameView = view.findViewById(R.id.package_name); - FlowLayout flowLayout = view.findViewById(R.id.tag_cloud); - MaterialAlertView warningView = view.findViewById(R.id.alert_text); - MaterialTextView descriptionView = view.findViewById(R.id.apk_description); - - Drawable icon = debloatObject.getIcon(); - appIconView.setImageDrawable(icon != null ? icon : requireActivity().getPackageManager().getDefaultActivityIcon()); - int[] users = debloatObject.getUsers(); - if (users != null && users.length > 0) { - openAppInfoButton.setOnClickListener(v -> { - Intent appDetailsIntent = AppDetailsActivity.getIntent(requireContext(), debloatObject.packageName, - users[0]); - startActivity(appDetailsIntent); - dismiss(); - }); - } else { - openAppInfoButton.setVisibility(View.GONE); - } - CharSequence label = debloatObject.getLabel(); - appLabelView.setText(label != null ? label : debloatObject.packageName); - packageNameView.setText(debloatObject.packageName); - String warning = debloatObject.getWarning(); - if (warning != null) { - warningView.setText(warning); - if (debloatObject.getRemoval() != DebloatObject.REMOVAL_CAUTION) { - warningView.setAlertType(MaterialAlertView.ALERT_TYPE_INFO); - } - } else warningView.setVisibility(View.GONE); - descriptionView.setText(getDescription(debloatObject.getDescription(), debloatObject.getWebRefs())); - // Add tags - int removalColor; - @StringRes - int removalRes; - switch (debloatObject.getRemoval()) { - case DebloatObject.REMOVAL_SAFE: - removalColor = ColorCodes.getRemovalSafeIndicatorColor(requireContext()); - removalRes = R.string.debloat_removal_safe_short_description; - break; - default: - case DebloatObject.REMOVAL_CAUTION: - removalColor = ColorCodes.getRemovalCautionIndicatorColor(requireContext()); - removalRes = R.string.debloat_removal_caution_short_description; - break; - case DebloatObject.REMOVAL_REPLACE: - removalColor = ColorCodes.getRemovalReplaceIndicatorColor(requireContext()); - removalRes = R.string.debloat_removal_replace_short_description; - break; - } - addTag(flowLayout, debloatObject.type); - addTag(flowLayout, removalRes, removalColor); - } - - @NonNull - private CharSequence getDescription(@NonNull String description, @Nullable String[] refSites) { - SpannableStringBuilder sb = new SpannableStringBuilder(); - sb.append(description.trim()); - if (refSites == null || refSites.length == 0) { - return sb; - } - // Add references - return sb.append(UIUtils.getBoldString("\n\nReferences\n")) - .append(UiUtils.getOrderedList(Arrays.asList(refSites))); - } - - private void addTag(@NonNull ViewGroup parent, @StringRes int titleRes, @ColorInt int textColor) { - Chip chip = (Chip) LayoutInflater.from(requireContext()).inflate(R.layout.item_chip, parent, false); - chip.setText(titleRes); - if (textColor >= 0) { - chip.setTextColor(textColor); - } - parent.addView(chip); - } - - private void addTag(@NonNull ViewGroup parent, @NonNull CharSequence title) { - Chip chip = (Chip) LayoutInflater.from(requireContext()).inflate(R.layout.item_chip, parent, false); - chip.setText(title); - parent.addView(chip); - } -} diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloatObject.java b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloatObject.java index a6ebef49468..f2eda45a42e 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloatObject.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloatObject.java @@ -12,6 +12,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; import io.github.muntashirakon.AppManager.utils.ArrayUtils; @@ -64,6 +65,8 @@ public class DebloatObject { private boolean mInstalled; @Nullable private Boolean mSystemApp = null; + @Nullable + private List mSuggestions; @Nullable public String[] getDependencies() { @@ -102,6 +105,20 @@ public String[] getWebRefs() { return ArrayUtils.defeatNullable(mWebRefs); } + @Nullable + public String getSuggestionId() { + return mSuggestionId; + } + + @Nullable + public List getSuggestions() { + return mSuggestions; + } + + public void setSuggestions(@Nullable List suggestions) { + mSuggestions = suggestions; + } + @Nullable public CharSequence getLabel() { return mLabel != null ? mLabel : mInternalLabel; diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterActivity.java b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterActivity.java index 8c0e3656458..cd7c77919cc 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterActivity.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterActivity.java @@ -86,7 +86,7 @@ protected void onAuthenticated(@Nullable Bundle savedInstanceState) { mMultiSelectionView.setOnItemSelectedListener(this); mMultiSelectionView.setOnSelectionChangeListener(this); - viewModel.getDebloatObjectLiveData().observe(this, debloatObjects -> { + viewModel.getDebloatObjectListLiveData().observe(this, debloatObjects -> { mProgressIndicator.hide(); adapter.setAdapterList(debloatObjects); }); diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterRecyclerViewAdapter.java b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterRecyclerViewAdapter.java index a3fdb12437c..131404a1b84 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterRecyclerViewAdapter.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterRecyclerViewAdapter.java @@ -129,8 +129,8 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { if (isInSelectionMode()) { toggleSelection(position); } else { - DebloatItemDetailsDialog dialog = DebloatItemDetailsDialog.getInstance(debloatObject.packageName); - dialog.show(mActivity.getSupportFragmentManager(), DebloatItemDetailsDialog.TAG); + BloatwareDetailsDialog dialog = BloatwareDetailsDialog.getInstance(debloatObject.packageName); + dialog.show(mActivity.getSupportFragmentManager(), BloatwareDetailsDialog.TAG); } }); holder.itemView.setCardBackgroundColor(mColorSurface); diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterViewModel.java b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterViewModel.java index c7a52bceae5..1b5f7993baf 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterViewModel.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/DebloaterViewModel.java @@ -14,7 +14,6 @@ import androidx.annotation.AnyThread; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; @@ -49,11 +48,14 @@ public class DebloaterViewModel extends AndroidViewModel { private String mQueryString = null; @AdvancedSearchView.SearchType private int mQueryType; - @Nullable - private List mDebloatObjects; + @NonNull + private final List mDebloatObjects = new ArrayList<>(); + @NonNull + private final HashMap> mIdSuggestionObjectsMap = new HashMap<>(); private final Map mSelectedPackages = new HashMap<>(); - private final MutableLiveData> mDebloatObjectLiveData = new MutableLiveData<>(); + private final MutableLiveData> mDebloatObjectListLiveData = new MutableLiveData<>(); + private final MutableLiveData mDebloatObjectLiveData = new MutableLiveData<>(); private final ExecutorService mExecutor = MultithreadedExecutor.getNewInstance(); private final Gson mGson = new Gson(); @@ -84,12 +86,16 @@ public void setQuery(String queryString, @AdvancedSearchView.SearchType int sear loadPackages(); } - public LiveData> getDebloatObjectLiveData() { + public LiveData> getDebloatObjectListLiveData() { + return mDebloatObjectListLiveData; + } + + public LiveData getDebloatObjectLiveData() { return mDebloatObjectLiveData; } public int getTotalItemCount() { - return mDebloatObjects != null ? mDebloatObjects.size() : 0; + return mDebloatObjects.size(); } public int getSelectedItemCount() { @@ -136,25 +142,23 @@ public ArrayList getSelectedPackagesWithUsers() { return userPackagePairs; } - @Nullable - public DebloatObject findDebloatObject(@NonNull String packageName) { - if (mDebloatObjects != null) { + public void findDebloatObject(@NonNull String packageName) { + mExecutor.submit(() -> { for (DebloatObject object : mDebloatObjects) { if (packageName.equals(object.packageName)) { - return object; + mDebloatObjectLiveData.postValue(object); + return; } } - } - return null; + mDebloatObjectLiveData.postValue(null); + + }); } @AnyThread public void loadPackages() { mExecutor.submit(() -> { loadDebloatObjects(); - if (mDebloatObjects == null) { - return; - } List debloatObjects = new ArrayList<>(); if (mFilterFlags != DebloaterListOptions.FILTER_NO_FILTER) { for (DebloatObject debloatObject : mDebloatObjects) { @@ -205,7 +209,7 @@ public void loadPackages() { } } if (TextUtils.isEmpty(mQueryString)) { - mDebloatObjectLiveData.postValue(debloatObjects); + mDebloatObjectListLiveData.postValue(debloatObjects); return; } // Apply searching @@ -219,28 +223,33 @@ public void loadPackages() { } }, mQueryType); - mDebloatObjectLiveData.postValue(newList); + mDebloatObjectListLiveData.postValue(newList); }); } @WorkerThread private void loadDebloatObjects() { - if (mDebloatObjects != null) { + if (!mDebloatObjects.isEmpty()) { return; } + loadSuggestions(); String jsonContent = FileUtils.getContentFromAssets(getApplication(), "debloat.json"); try { - mDebloatObjects = Arrays.asList(mGson.fromJson(jsonContent, DebloatObject[].class)); + mDebloatObjects.addAll(Arrays.asList(mGson.fromJson(jsonContent, DebloatObject[].class))); } catch (Throwable e) { e.printStackTrace(); } - if (mDebloatObjects == null) { + if (mDebloatObjects.isEmpty()) { return; } PackageManager pm = getApplication().getPackageManager(); // Fetch package info for all users AppDb appDb = new AppDb(); for (DebloatObject debloatObject : mDebloatObjects) { + // Update suggestion data + List suggestionObjects = mIdSuggestionObjectsMap.get(debloatObject.getSuggestionId()); + debloatObject.setSuggestions(suggestionObjects); + // Update application data List apps = appDb.getAllApplications(debloatObject.packageName); for (App app : apps) { if (!app.isInstalled) { @@ -254,11 +263,7 @@ private void loadDebloatObjects() { try { ApplicationInfo ai = PackageManagerCompat.getApplicationInfo(debloatObject.packageName, MATCH_UNINSTALLED_PACKAGES | MATCH_STATIC_SHARED_AND_SDK_LIBRARIES, app.userId); - if ((ai.flags & ApplicationInfo.FLAG_INSTALLED) != 0) { - // Reset installed - debloatObject.setInstalled(true); - } - // Reset system app + debloatObject.setInstalled((ai.flags & ApplicationInfo.FLAG_INSTALLED) != 0); debloatObject.setSystemApp(ApplicationInfoCompat.isSystemApp(ai)); debloatObject.setLabel(ai.loadLabel(pm)); debloatObject.setIcon(ai.loadIcon(pm)); @@ -268,4 +273,34 @@ private void loadDebloatObjects() { } } } + + @WorkerThread + private void loadSuggestions() { + if (!mIdSuggestionObjectsMap.isEmpty()) { + return; + } + String jsonContent = FileUtils.getContentFromAssets(getApplication(), "suggestions.json"); + try { + SuggestionObject[] suggestionObjects = mGson.fromJson(jsonContent, SuggestionObject[].class); + if (suggestionObjects != null) { + AppDb appDb = new AppDb(); + for (SuggestionObject suggestionObject : suggestionObjects) { + List objects = mIdSuggestionObjectsMap.get(suggestionObject.suggestionId); + if (objects == null) { + objects = new ArrayList<>(); + mIdSuggestionObjectsMap.put(suggestionObject.suggestionId, objects); + } + objects.add(suggestionObject); + List apps = appDb.getAllApplications(suggestionObject.packageName); + for (App app : apps) { + if (app.isInstalled) { + suggestionObject.addUser(app.userId); + } + } + } + } + } catch (Throwable th) { + th.printStackTrace(); + } + } } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/debloat/SuggestionObject.java b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/SuggestionObject.java new file mode 100644 index 00000000000..265c027122f --- /dev/null +++ b/app/src/main/java/io/github/muntashirakon/AppManager/debloat/SuggestionObject.java @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package io.github.muntashirakon.AppManager.debloat; + +import android.content.Intent; +import android.net.Uri; + +import androidx.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +import io.github.muntashirakon.AppManager.utils.ArrayUtils; + +public class SuggestionObject { + @SerializedName("_id") + public String suggestionId; + @SerializedName("id") + public String packageName; + + @SerializedName("label") + private String mLabel; + @SerializedName("reason") + @Nullable + private String mReason; + @SerializedName("source") + private String mSource; + @SerializedName("repo") + private String mRepo; + + private int[] mUsers; + + public String getLabel() { + return mLabel; + } + + public String getRepo() { + return mRepo; + } + + @Nullable + public String getReason() { + return mReason; + } + + public boolean isInFDroidMarket() { + return mSource != null && mSource.contains("f"); + } + + public Intent getMarketLink() { + // Not supported by most app stores + // return new Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q=pname:" + packageName + " " + mLabel)); + Uri uri; + if (isInFDroidMarket()) { + uri = Uri.parse("https://f-droid.org/packages/" + packageName); + } else uri = Uri.parse("https://play.google.com/store/apps/details?id=" + packageName); + return new Intent(Intent.ACTION_VIEW, uri); + } + + public int[] getUsers() { + return mUsers; + } + + public void addUser(int userId) { + if (mUsers == null) { + mUsers = new int[]{userId}; + } else { + mUsers = ArrayUtils.appendInt(mUsers, userId); + } + } +} diff --git a/app/src/main/res/layout/dialog_debloat_item_details.xml b/app/src/main/res/layout/dialog_bloatware_details.xml similarity index 78% rename from app/src/main/res/layout/dialog_debloat_item_details.xml rename to app/src/main/res/layout/dialog_bloatware_details.xml index f028eea75d4..2b7fbc853c9 100644 --- a/app/src/main/res/layout/dialog_debloat_item_details.xml +++ b/app/src/main/res/layout/dialog_bloatware_details.xml @@ -71,7 +71,6 @@ android:id="@+id/tag_cloud" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingHorizontal="@dimen/padding_medium" android:gravity="start" tools:listItem="@layout/item_chip" /> @@ -89,8 +88,31 @@ android:layout_marginTop="8dp" android:textAppearance="?attr/textAppearanceListItemSecondary" android:autoLink="web" - tools:text="@tools:sample/lorem/random" - tools:ignore="SmallSp" /> + android:textIsSelectable="true" + tools:text="@tools:sample/lorem/random" /> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_bloatware_details.xml b/app/src/main/res/layout/item_bloatware_details.xml new file mode 100644 index 00000000000..f8c348457d5 --- /dev/null +++ b/app/src/main/res/layout/item_bloatware_details.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b9f746f9209..f02fa303c56 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1422,4 +1422,5 @@ Safe to remove Exercise caution Replace with alternative + Alternatives diff --git a/scripts/make_debloat_list.php b/scripts/make_debloat_list.php index 8eac74f32a1..c68104efad1 100644 --- a/scripts/make_debloat_list.php +++ b/scripts/make_debloat_list.php @@ -5,29 +5,22 @@ const SUPPORTED_REMOVAL_TYPES = ['delete', 'replace', 'caution', 'unsafe']; const SUPPORTED_TAGS = []; +const REPO_DIR = __DIR__ . '/android-debloat-list'; -$repo_dir = __DIR__ . '/android-debloat-list'; $target_file = __DIR__ . '/../app/src/main/assets/debloat.json'; -$debloat_file_list = array(); -$debloat_file_type = array(); -foreach (list_files($repo_dir) as $filename) { - if (str_ends_with($filename, ".json")) { - $debloat_file_list[] = $repo_dir . '/' . $filename; - $debloat_file_type[] = substr($filename, 0, -5); - } -} - $debloat_list = array(); -$file_count = count($debloat_file_list); -for ($i = 0; $i < $file_count; ++$i) { - $file = $debloat_file_list[$i]; - $type = $debloat_file_type[$i]; +foreach (list_files(REPO_DIR) as $filename) { + if (!str_ends_with($filename, ".json")) { + continue; + } + $file = REPO_DIR . '/' . $filename; + $type = substr($filename, 0, -5); $list = json_decode(file_get_contents($file), true); if ($list === null) { fprintf(STDERR, "Malformed file: $file\n"); continue; - } else fprintf(STDERR, "Adding $file\n"); + } else fprintf(STDERR, "Adding $filename\n"); foreach ($list as $item) { verify_item($item); if ($item['removal'] == 'unsafe') { @@ -115,7 +108,14 @@ function verify_item(array $item): void { fprintf(STDERR, "{$item['id']}: Expected `warning` field to be a string, found: " . gettype($item['warning']) . "\n"); } // `warning` is an optional string - if (isset($item['suggestions']) && gettype($item['suggestions']) != 'string') { - fprintf(STDERR, "{$item['id']}: Expected `suggestions` field to be a string, found: " . gettype($item['suggestions']) . "\n"); + if (isset($item['suggestions'])) { + if (gettype($item['suggestions']) != 'string') { + fprintf(STDERR, "{$item['id']}: Expected `suggestions` field to be a string, found: " . gettype($item['suggestions']) . "\n"); + } else { + $suggestion_file = REPO_DIR . '/suggestions/' . $item['suggestions'] . '.json'; + if (!file_exists($suggestion_file)) { + fprintf(STDERR, "{$item['id']}: Suggestion ID ({$item['suggestions']}) does not exist.\n"); + } + } } } diff --git a/scripts/make_suggestions.php b/scripts/make_suggestions.php new file mode 100644 index 00000000000..af40ba54338 --- /dev/null +++ b/scripts/make_suggestions.php @@ -0,0 +1,57 @@ +