...
 
Commits (20)
......@@ -9,13 +9,14 @@ Yet another android package manager and viewer but...
- It tries to display as much information as possible in the main window
- It lists activities, broadcast receivers, services, providers, permissions, signatures, shared libraries, etc. of any app
- It can launch (exportable) activities, create (customizable) shortcuts
- It can block any activities, broadcast receivers, services or providers you like (requires root)
- It can block any activities, broadcast receivers, services or providers you like with Watt and Blocker import support (requires root)
- It can revoke permissions that are considered dangerous (requires root)
- It can disable app ops that are considered dangerous (requires root)
- It can scan for trackers in apps and list (all or only) tracking classes (and their code dump)
- It can generate dynamic manifest for any app
- It can be used to view/edit/delete shared preferences of any app (requires root)
- It displays your app usage, app storage info (requires “Usage Access” permission)
- It displays running processes/apps (requires root)
- It displays your app usage, data usage and app storage info (requires “Usage Access” permission)
- Apk files can be shared (hence the use of a provider)
- It can be used to clear app data or app cache (requires root)
......
......@@ -50,11 +50,8 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.android.material:material:1.3.0-alpha01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.jaredrummler:android-shell:1.0.0'
// implementation 'com.github.kbiakov:CodeView-Android:1.3.2'
}
......@@ -7,6 +7,7 @@
android:maxSdkVersion="25" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
......@@ -16,6 +17,7 @@
android:theme="@style/AppTheme">
<activity
android:name=".activities.RunningAppsActivity"
android:label="@string/running_apps"
android:exported="false" />
<activity
android:name=".activities.SettingsActivity"
......
......@@ -58,7 +58,7 @@ public class MainLoader extends AsyncTaskLoader<List<ApplicationItem>> {
item.size = (long) -1 * applicationInfo.targetSdkVersion;
}
if (isRootEnabled) {
item.blockedCount = ComponentsBlocker.getInstance(getContext(), pName)
item.blockedCount = ComponentsBlocker.getInstance(getContext(), pName, true)
.componentCount();
}
itemList.add(item);
......@@ -87,7 +87,7 @@ public class MainLoader extends AsyncTaskLoader<List<ApplicationItem>> {
}
if (isRootEnabled) {
item.blockedCount = ComponentsBlocker.getInstance(getContext(),
applicationInfo.packageName).componentCount();
applicationInfo.packageName, true).componentCount();
}
itemList.add(item);
}
......
......@@ -44,6 +44,7 @@ public class AppDetailsActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_details);
setSupportActionBar(findViewById(R.id.toolbar));
mConstraint = null;
mPackageName = getIntent().getStringExtra(AppInfoActivity.EXTRA_PACKAGE_NAME);
if (mPackageName == null) {
......
......@@ -32,7 +32,6 @@ import android.widget.Toast;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.classysharkandroid.utils.IOUtils;
import com.jaredrummler.android.shell.Shell;
import java.io.File;
import java.io.FileInputStream;
......@@ -58,6 +57,7 @@ import androidx.core.content.FileProvider;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import io.github.muntashirakon.AppManager.BuildConfig;
import io.github.muntashirakon.AppManager.R;
import io.github.muntashirakon.AppManager.runner.Runner;
import io.github.muntashirakon.AppManager.usage.AppUsageStatsManager;
import io.github.muntashirakon.AppManager.utils.AppPref;
import io.github.muntashirakon.AppManager.utils.ListItemCreator;
......@@ -101,6 +101,7 @@ public class AppInfoActivity extends AppCompatActivity implements SwipeRefreshLa
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_info);
setSupportActionBar(findViewById(R.id.toolbar));
mPackageName = getIntent().getStringExtra(AppInfoActivity.EXTRA_PACKAGE_NAME);
if (mPackageName == null) {
Toast.makeText(this, getString(R.string.empty_package_name), Toast.LENGTH_LONG).show();
......@@ -116,7 +117,6 @@ public class AppInfoActivity extends AppCompatActivity implements SwipeRefreshLa
mTagCloud = findViewById(R.id.tag_cloud);
mAccentColor = Utils.getThemeColor(this, android.R.attr.colorAccent);
mProgressBar = findViewById(R.id.progress_horizontal);
getPackageInfoOrFinish();
}
@Override
......@@ -250,33 +250,37 @@ public class AppInfoActivity extends AppCompatActivity implements SwipeRefreshLa
// Set uninstall
addToHorizontalLayout(R.string.uninstall, R.drawable.ic_delete_black_24dp).setOnClickListener(v -> {
final boolean isSystemApp = (mApplicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
// FIXME: Uninstall for all users
final Boolean isRootEnabled = (Boolean) AppPref.get(this, AppPref.PREF_ROOT_MODE_ENABLED, AppPref.TYPE_BOOLEAN);
new AlertDialog.Builder(this, R.style.CustomDialog)
.setTitle(mPackageLabel)
.setMessage(isSystemApp ?
R.string.uninstall_system_app_message : R.string.uninstall_app_message)
.setPositiveButton(R.string.uninstall, (dialog, which) -> {
// Try without root first then with root
if (Shell.SH.run(String.format("pm uninstall --user 0 %s", mPackageName)).isSuccessful()
|| (isRootEnabled && Shell.SU.run(String.format("pm uninstall --user 0 %s", mPackageName)).isSuccessful())) {
Toast.makeText(mActivity, String.format(getString(R.string.uninstalled_successfully), mPackageLabel), Toast.LENGTH_LONG).show();
finish();
} else {
Toast.makeText(mActivity, String.format(getString(R.string.failed_to_uninstall), mPackageLabel), Toast.LENGTH_LONG).show();
}
})
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
if (dialog != null) dialog.cancel();
})
.show();
if (isRootEnabled) {
new AlertDialog.Builder(this, R.style.CustomDialog)
.setTitle(mPackageLabel)
.setMessage(isSystemApp ?
R.string.uninstall_system_app_message : R.string.uninstall_app_message)
.setPositiveButton(R.string.uninstall, (dialog, which) -> {
// Try without root first then with root
if (Runner.run(this, String.format("pm uninstall --user 0 %s", mPackageName)).isSuccessful()) {
Toast.makeText(mActivity, String.format(getString(R.string.uninstalled_successfully), mPackageLabel), Toast.LENGTH_LONG).show();
finish();
} else {
Toast.makeText(mActivity, String.format(getString(R.string.failed_to_uninstall), mPackageLabel), Toast.LENGTH_LONG).show();
}
})
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
if (dialog != null) dialog.cancel();
})
.show();
} else {
Intent uninstallIntent = new Intent(Intent.ACTION_DELETE);
uninstallIntent.setData(Uri.parse("package:" + mPackageName));
startActivity(uninstallIntent);
}
});
// Enable/disable app (root only)
if ((Boolean) AppPref.get(this, AppPref.PREF_ROOT_MODE_ENABLED, AppPref.TYPE_BOOLEAN)) {
if (mApplicationInfo.enabled) {
// Disable app
addToHorizontalLayout(R.string.disable, R.drawable.ic_block_black_24dp).setOnClickListener(v -> {
if (Shell.SU.run(String.format("pm disable %s", mPackageName)).isSuccessful()) {
if (Runner.run(this, String.format("pm disable %s", mPackageName)).isSuccessful()) {
// Refresh
getPackageInfoOrFinish();
} else {
......@@ -286,7 +290,7 @@ public class AppInfoActivity extends AppCompatActivity implements SwipeRefreshLa
} else {
// Enable app
addToHorizontalLayout(R.string.enable, R.drawable.ic_baseline_get_app_24).setOnClickListener(v -> {
if (Shell.SU.run(String.format("pm enable %s", mPackageName)).isSuccessful()) {
if (Runner.run(this, String.format("pm enable %s", mPackageName)).isSuccessful()) {
// Refresh
getPackageInfoOrFinish();
} else {
......@@ -294,20 +298,18 @@ public class AppInfoActivity extends AppCompatActivity implements SwipeRefreshLa
}
});
}
// Force stop
if ((mApplicationInfo.flags & ApplicationInfo.FLAG_STOPPED) == 0) {
addToHorizontalLayout(R.string.force_stop, R.drawable.ic_baseline_power_settings_new_24).setOnClickListener(v -> {
if (Runner.run(this, String.format("am force-stop %s", mPackageName)).isSuccessful()) {
// Refresh
getPackageInfoOrFinish();
} else {
Toast.makeText(mActivity, String.format(getString(R.string.failed_to_stop), mPackageLabel), Toast.LENGTH_LONG).show();
}
});
}
} // End root only
// Force stop
if ((mApplicationInfo.flags & ApplicationInfo.FLAG_STOPPED) == 0) {
addToHorizontalLayout(R.string.force_stop, R.drawable.ic_baseline_power_settings_new_24).setOnClickListener(v -> {
final Boolean isRootEnabled = (Boolean) AppPref.get(this, AppPref.PREF_ROOT_MODE_ENABLED, AppPref.TYPE_BOOLEAN);
if (Shell.SH.run(String.format("am force-stop %s", mPackageName)).isSuccessful()
|| (isRootEnabled && Shell.SU.run(String.format("am force-stop %s", mPackageName)).isSuccessful())) {
// Refresh
getPackageInfoOrFinish();
} else {
Toast.makeText(mActivity, String.format(getString(R.string.failed_to_stop), mPackageLabel), Toast.LENGTH_LONG).show();
}
});
}
// Set manifest
addToHorizontalLayout(R.string.manifest, R.drawable.ic_tune_black_24dp).setOnClickListener(v -> {
Intent intent = new Intent(mActivity, ManifestViewerActivity.class);
......@@ -551,13 +553,13 @@ public class AppInfoActivity extends AppCompatActivity implements SwipeRefreshLa
private List<String> getSharedPrefs(@NonNull String sourceDir) {
File sharedPath = new File(sourceDir + "/shared_prefs");
return Shell.SU.run(String.format("ls %s/*.xml", sharedPath.getAbsolutePath())).stdout;
return Runner.run(this, String.format("ls %s/*.xml", sharedPath.getAbsolutePath())).getOutputAsList();
}
private List<String> getDatabases(@NonNull String sourceDir) {
File sharedPath = new File(sourceDir + "/databases");
// FIXME: SQLite db doesn't necessarily have .db extension
return Shell.SU.run(String.format("ls %s/*.db", sharedPath.getAbsolutePath())).stdout;
return Runner.run(this, String.format("ls %s/*.db", sharedPath.getAbsolutePath())).getOutputAsList();
}
private void openAsFolderInFM(String dir) {
......@@ -655,7 +657,7 @@ public class AppInfoActivity extends AppCompatActivity implements SwipeRefreshLa
if (isRootEnabled) {
mList.setOpen(v -> {
// Clear data
if (Shell.SU.run(String.format("pm clear %s", mPackageName)).isSuccessful()) {
if (Runner.run(this, String.format("pm clear %s", mPackageName)).isSuccessful()) {
getPackageInfoOrFinish();
}
});
......@@ -679,7 +681,7 @@ public class AppInfoActivity extends AppCompatActivity implements SwipeRefreshLa
String extCache = cacheDir.getAbsolutePath().replace(getPackageName(), mPackageName);
command.append(" ").append(extCache);
}
if (Shell.SU.run(command.toString()).isSuccessful()) {
if (Runner.run(this, command.toString()).isSuccessful()) {
getPackageInfoOrFinish();
}
});
......
......@@ -12,6 +12,7 @@ import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.text.format.Formatter;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
......@@ -42,6 +43,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.menu.MenuBuilder;
import io.github.muntashirakon.AppManager.R;
import io.github.muntashirakon.AppManager.fragments.AppUsageDetailsDialogFragment;
import io.github.muntashirakon.AppManager.usage.AppUsageStatsManager;
import io.github.muntashirakon.AppManager.usage.Utils.IntervalType;
import io.github.muntashirakon.AppManager.utils.Utils;
......@@ -51,7 +53,7 @@ import static io.github.muntashirakon.AppManager.usage.Utils.USAGE_TODAY;
import static io.github.muntashirakon.AppManager.usage.Utils.USAGE_WEEKLY;
import static io.github.muntashirakon.AppManager.usage.Utils.USAGE_YESTERDAY;
public class AppUsageActivity extends AppCompatActivity {
public class AppUsageActivity extends AppCompatActivity implements ListView.OnItemClickListener {
@IntDef(value = {
SORT_BY_APP_LABEL,
SORT_BY_LAST_USED,
......@@ -84,6 +86,7 @@ public class AppUsageActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_usage);
setSupportActionBar(findViewById(R.id.toolbar));
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(getString(R.string.app_usage));
......@@ -95,6 +98,7 @@ public class AppUsageActivity extends AppCompatActivity {
listView.setDividerHeight(0);
listView.setEmptyView(findViewById(android.R.id.empty));
listView.setAdapter(mAppUsageAdapter);
listView.setOnItemClickListener(this);
@SuppressLint("InflateParams")
View header = getLayoutInflater().inflate(R.layout.header_app_usage, null);
......@@ -176,6 +180,18 @@ public class AppUsageActivity extends AppCompatActivity {
return super.onOptionsItemSelected(item);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
AppUsageStatsManager.PackageUS packageUS = mAppUsageAdapter.getItem(position-1);
AppUsageStatsManager.PackageUS packageUS1 = AppUsageStatsManager.getInstance(this).getUsageStatsForPackage(packageUS.packageName, current_interval);
packageUS1.copyOthers(packageUS);
AppUsageDetailsDialogFragment appUsageDetailsDialogFragment = new AppUsageDetailsDialogFragment();
Bundle args = new Bundle();
args.putParcelable(AppUsageDetailsDialogFragment.ARG_PACKAGE_US, packageUS1);
appUsageDetailsDialogFragment.setArguments(args);
appUsageDetailsDialogFragment.show(getSupportFragmentManager(), AppUsageDetailsDialogFragment.TAG);
}
private void setSortBy(@SortOrder int sort) {
mSortBy = sort;
sortPackageUSList();
......@@ -291,11 +307,9 @@ public class AppUsageActivity extends AppCompatActivity {
TextView appLabel;
TextView packageName;
TextView lastUsageDate;
TextView timesOpened;
TextView mobileDataUsage;
TextView wifiDataUsage;
TextView screenTime;
TextView notificationCount;
IconAsyncTask iconLoader;
}
......@@ -316,7 +330,7 @@ public class AppUsageActivity extends AppCompatActivity {
}
@Override
public Object getItem(int position) {
public AppUsageStatsManager.PackageUS getItem(int position) {
return mAdapterList.get(position);
}
......@@ -336,11 +350,9 @@ public class AppUsageActivity extends AppCompatActivity {
holder.appLabel = convertView.findViewById(R.id.label);
holder.packageName = convertView.findViewById(R.id.package_name);
holder.lastUsageDate = convertView.findViewById(R.id.date);
holder.timesOpened = convertView.findViewById(R.id.times_opened);
holder.mobileDataUsage = convertView.findViewById(R.id.data_usage);
holder.wifiDataUsage = convertView.findViewById(R.id.wifi_usage);
holder.screenTime = convertView.findViewById(R.id.screen_time);
holder.notificationCount = convertView.findViewById(R.id.notification_count);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
......@@ -365,12 +377,14 @@ public class AppUsageActivity extends AppCompatActivity {
if (lastTimeUsed > 1) {
holder.lastUsageDate.setText(sSimpleDateFormat.format(new Date(lastTimeUsed)));
}
String screenTimesWithTimesOpened;
// Set times opened
holder.timesOpened.setText(String.format(packageUS.timesOpened == 1 ?
screenTimesWithTimesOpened = String.format(packageUS.timesOpened == 1 ?
mActivity.getString(R.string.one_time_opened)
: mActivity.getString(R.string.no_of_times_opened), packageUS.timesOpened));
: mActivity.getString(R.string.no_of_times_opened), packageUS.timesOpened);
// Set screen time
holder.screenTime.setText(formattedTime(mActivity, packageUS.screenTime));
screenTimesWithTimesOpened += ", " + formattedTime(mActivity, packageUS.screenTime);
holder.screenTime.setText(screenTimesWithTimesOpened);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Set data usage
holder.mobileDataUsage.setText("M: \u2191 " + Formatter.formatFileSize(mActivity, packageUS.mobileData.getFirst())
......@@ -378,11 +392,6 @@ public class AppUsageActivity extends AppCompatActivity {
holder.wifiDataUsage.setText("W: \u2191 " + Formatter.formatFileSize(mActivity, packageUS.wifiData.getFirst())
+ " \u2193 " + Formatter.formatFileSize(mActivity, packageUS.wifiData.getSecond()));
}
// Set notification count
// holder.notificationCount.setText(String.format(packageUS.notificationReceived == 1 ?
// mActivity.getString(R.string.one_notification_received)
// : mActivity.getString(R.string.no_of_notification_received),
// packageUS.notificationReceived));
return convertView;
}
......
......@@ -118,6 +118,7 @@ public class ClassListingActivity extends AppCompatActivity implements SearchVie
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_class_listing);
setSupportActionBar(findViewById(R.id.toolbar));
mActionBar = getSupportActionBar();
if (mActionBar != null) {
mPackageName = getIntent().getStringExtra(EXTRA_PACKAGE_NAME);
......
......@@ -55,6 +55,7 @@ public class ManifestViewerActivity extends AppCompatActivity {
setContentView(R.layout.activity_any_viewer_wrapped);
else
setContentView(R.layout.activity_any_viewer);
setSupportActionBar(findViewById(R.id.toolbar));
mProgressBar = findViewById(R.id.progress_horizontal);
......
......@@ -20,6 +20,7 @@ public class SettingsActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
setSupportActionBar(findViewById(R.id.toolbar));
appPref = AppPref.getInstance(this);
final SwitchMaterial rootSwitcher = findViewById(R.id.root_toggle_btn);
......
......@@ -21,7 +21,6 @@ import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.jaredrummler.android.shell.Shell;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
......@@ -46,6 +45,7 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import io.github.muntashirakon.AppManager.R;
import io.github.muntashirakon.AppManager.fragments.EditPrefItemFragment;
import io.github.muntashirakon.AppManager.runner.Runner;
import io.github.muntashirakon.AppManager.utils.Utils;
public class SharedPrefsActivity extends AppCompatActivity implements
......@@ -73,6 +73,7 @@ public class SharedPrefsActivity extends AppCompatActivity implements
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_shared_prefs);
setSupportActionBar(findViewById(R.id.toolbar));
mSharedPrefFile = getIntent().getStringExtra(EXTRA_PREF_LOCATION);
String appLabel = getIntent().getStringExtra(EXTRA_PREF_LABEL);
if (mSharedPrefFile == null) {
......@@ -172,7 +173,7 @@ public class SharedPrefsActivity extends AppCompatActivity implements
return true;
case R.id.action_delete:
// Make sure it's a file and then delete
boolean isSuccess = Shell.SU.run(String.format("[ -f '%s' ] && rm -f '%s'",
boolean isSuccess = Runner.run(this, String.format("[ -f '%s' ] && rm -f '%s'",
mSharedPrefFile, mSharedPrefFile)).isSuccessful();
if (isSuccess) {
Toast.makeText(this, R.string.deleted_successfully, Toast.LENGTH_LONG).show();
......@@ -281,7 +282,7 @@ public class SharedPrefsActivity extends AppCompatActivity implements
@Override
public void run() {
String sharedPrefPath = mTempSharedPrefFile.getAbsolutePath();
if(!Shell.SU.run(String.format("cp '%s' '%s' && chmod 0666 '%s'", mSharedPrefFile,
if(!Runner.run(SharedPrefsActivity.this, String.format("cp '%s' '%s' && chmod 0666 '%s'", mSharedPrefFile,
sharedPrefPath, sharedPrefPath)).isSuccessful()) {
runOnUiThread(SharedPrefsActivity.this::finish);
}
......@@ -336,7 +337,7 @@ public class SharedPrefsActivity extends AppCompatActivity implements
xmlSerializer.flush();
xmlFile.write(stringWriter.toString().getBytes());
xmlFile.close();
return Shell.SU.run(String.format("cp '%s' '%s' && chmod 0666 '%s'", sharedPrefsFile,
return Runner.run(this, String.format("cp '%s' '%s' && chmod 0666 '%s'", sharedPrefsFile,
mSharedPrefFile, mSharedPrefFile)).isSuccessful();
} catch (IOException e) {
e.printStackTrace();
......
package io.github.muntashirakon.AppManager.appops;
import android.annotation.SuppressLint;
import com.jaredrummler.android.shell.CommandResult;
import com.jaredrummler.android.shell.Shell;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
......@@ -12,6 +10,7 @@ import java.util.regex.Pattern;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.muntashirakon.AppManager.runner.Runner;
@SuppressLint("DefaultLocale")
public
......@@ -33,6 +32,10 @@ class AppOpsService implements IAppOpsService {
private boolean isSuccessful = false;
private List<String> output = null;
private Context context;
public AppOpsService(Context context) {
this.context = context;
}
/**
* Get the mode of operation of the given package or uid.
......@@ -131,9 +134,9 @@ class AppOpsService implements IAppOpsService {
* @param command The command to run
*/
private void runCommand(String command) {
CommandResult commandResult = Shell.SU.run(command);
isSuccessful = commandResult.isSuccessful();
output = commandResult.stdout;
Runner.Result result = Runner.run(context, command);
isSuccessful = result.isSuccessful();
output = result.getOutputAsList();
}
/**
......
package io.github.muntashirakon.AppManager.compontents;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ServiceInfo;
import android.net.Uri;
import android.os.Build;
import android.os.FileUtils;
import android.util.Log;
import com.google.classysharkandroid.utils.IOUtils;
......@@ -21,11 +25,10 @@ import java.util.HashMap;
import java.util.List;
import androidx.annotation.NonNull;
import io.github.muntashirakon.AppManager.storage.StorageManager;
import io.github.muntashirakon.AppManager.utils.Tuple;
import io.github.muntashirakon.AppManager.utils.Utils;
import static io.github.muntashirakon.AppManager.compontents.ComponentsBlocker.ComponentType;
/**
* Import components from external apps like Blocker, MyAndroidTools, Watt
*/
......@@ -65,6 +68,8 @@ public class ExternalComponentsImporter {
/**
* Watt only supports IFW, so copy them directly
*
* FIXME: Breaks in v2.5.6
* @param context Application context
* @param fileUri File URI
*/
......@@ -92,7 +97,6 @@ public class ExternalComponentsImporter {
/**
* Apply from blocker
* FIXME: Retrieve component types using PackageInfo instead of json file
* @param context Application context
* @param uri File URI
*/
......@@ -100,23 +104,28 @@ public class ExternalComponentsImporter {
throws Exception {
try {
String jsonString = Utils.getFileContent(context.getContentResolver(), uri);
HashMap<String, HashMap<String, ComponentType>> packageComponents = new HashMap<>();
HashMap<String, HashMap<String, StorageManager.Type>> packageComponents = new HashMap<>();
HashMap<String, PackageInfo> packageInfoList = new HashMap<>();
PackageManager packageManager = context.getPackageManager();
JSONObject jsonObject = new JSONObject(jsonString);
JSONArray components = jsonObject.getJSONArray("components");
for(int i = 0; i<components.length(); ++i) {
JSONObject component = (JSONObject) components.get(i);
String packageName = component.getString("packageName");
if (!packageInfoList.containsKey(packageName))
packageInfoList.put(packageName, packageManager.getPackageInfo(packageName,
PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS
| PackageManager.GET_PROVIDERS | PackageManager.GET_SERVICES));
String componentName = component.getString("name");
String type = component.getString("type");
if (!packageComponents.containsKey(packageName))
packageComponents.put(packageName, new HashMap<>());
//noinspection ConstantConditions
packageComponents.get(packageName).put(componentName, getTypeFromString(type));
packageComponents.get(packageName).put(componentName, getType(componentName, packageInfoList.get(packageName)));
}
if (packageComponents.size() > 0) {
ComponentsBlocker blocker;
for (String packageName: packageComponents.keySet()) {
HashMap<String, ComponentType> disabledComponents = packageComponents.get(packageName);
HashMap<String, StorageManager.Type> disabledComponents = packageComponents.get(packageName);
//noinspection ConstantConditions
if (disabledComponents.size() > 0) {
blocker = ComponentsBlocker.getInstance(context, packageName);
......@@ -133,14 +142,15 @@ public class ExternalComponentsImporter {
}
}
@NonNull
private static ComponentType getTypeFromString(@NonNull String strType) {
switch (strType) {
case "ACTIVITY": return ComponentType.ACTIVITY;
case "PROVIDER": return ComponentType.PROVIDER;
case "RECEIVER": return ComponentType.RECEIVER;
case "SERVICE": return ComponentType.SERVICE;
}
return ComponentType.UNKNOWN;
private static StorageManager.Type getType(@NonNull String name, @NonNull PackageInfo packageInfo) {
for (ActivityInfo activityInfo: packageInfo.activities)
if (activityInfo.name.equals(name)) return StorageManager.Type.ACTIVITY;
for (ProviderInfo providerInfo: packageInfo.providers)
if (providerInfo.name.equals(name)) return StorageManager.Type.PROVIDER;
for (ActivityInfo receiverInfo: packageInfo.receivers)
if (receiverInfo.name.equals(name)) return StorageManager.Type.RECEIVER;
for (ServiceInfo serviceInfo: packageInfo.services)
if (serviceInfo.name.equals(name)) return StorageManager.Type.SERVICE;
return StorageManager.Type.UNKNOWN;
}
}
package io.github.muntashirakon.AppManager.fragments;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import org.w3c.dom.Text;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import io.github.muntashirakon.AppManager.R;
import io.github.muntashirakon.AppManager.activities.AppUsageActivity;
import io.github.muntashirakon.AppManager.usage.AppUsageStatsManager;
public class AppUsageDetailsDialogFragment extends DialogFragment {
public static final String TAG = "AppUsageDetailsDialogFragment";
public static final String ARG_PACKAGE_US = "ARG_PACKAGE_US";
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
if (getActivity() == null || getContext() == null || getArguments() == null)
return super.onCreateDialog(savedInstanceState);
AppUsageStatsManager.PackageUS packageUS = getArguments().getParcelable(ARG_PACKAGE_US);
LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (packageUS == null || inflater == null) return super.onCreateDialog(savedInstanceState);
@SuppressLint("InflateParams")
View view = inflater.inflate(R.layout.dialog_app_usage_details, null);
ListView listView = view.findViewById(android.R.id.list);
listView.setDividerHeight(0);
TextView emptyView = view.findViewById(android.R.id.empty);
listView.setEmptyView(emptyView);
AppUsageDetailsAdapter adapter = new AppUsageDetailsAdapter(getActivity());
listView.setAdapter(adapter);
adapter.setDefaultList(packageUS.entries);
AlertDialog.Builder builder = new AlertDialog.Builder(getContext(), R.style.CustomDialog)
.setTitle(packageUS.packageName)
.setView(view)
.setNegativeButton(android.R.string.ok, (dialog, which) -> {
if (getDialog() == null) dismiss();
});
try {
PackageManager packageManager = getActivity().getPackageManager();
ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageUS.packageName, PackageManager.GET_META_DATA);
builder.setIcon(applicationInfo.loadIcon(packageManager));
builder.setTitle(applicationInfo.loadLabel(packageManager));
} catch (PackageManager.NameNotFoundException e) {
builder.setTitle(packageUS.packageName);
}
return builder.create();
}
static class AppUsageDetailsAdapter extends BaseAdapter {
static DateFormat sSimpleDateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss", Locale.getDefault());
private LayoutInflater mLayoutInflater;
private List<AppUsageStatsManager.USEntry> mDefaultList;
private List<AppUsageStatsManager.USEntry> mAdapterList;
private Context context;
private int mColorTransparent;
private int mColorSemiTransparent;
AppUsageDetailsAdapter(@NonNull Activity activity) {
context = activity;
mLayoutInflater = activity.getLayoutInflater();
mColorTransparent = Color.TRANSPARENT;
mColorSemiTransparent = ContextCompat.getColor(activity, R.color.SEMI_TRANSPARENT);
}
void setDefaultList(List<AppUsageStatsManager.USEntry> list) {
mDefaultList = list;
mAdapterList = list;
notifyDataSetChanged();
}
@Override
public int getCount() {
return mAdapterList == null ? 0 : mAdapterList.size();
}
@Override
public AppUsageStatsManager.USEntry getItem(int position) {
return mAdapterList.get(position);
}
@Override
public long getItemId(int position) {
return mDefaultList.indexOf(mAdapterList.get(position));
}
static class ViewHolder {
TextView title;
TextView subtitle;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mLayoutInflater.inflate(R.layout.item_app_usage_details, parent, false);
holder = new ViewHolder();
holder.title = convertView.findViewById(R.id.item_title);
holder.subtitle = convertView.findViewById(R.id.item_subtitle);
convertView.setTag(holder);
} else holder = (ViewHolder) convertView.getTag();
AppUsageStatsManager.USEntry usEntry = mAdapterList.get(position);
holder.title.setText(String.format(Locale.ROOT, "%s - %s", sSimpleDateFormat.format(usEntry.startTime), sSimpleDateFormat.format(usEntry.endTime)));
holder.subtitle.setText(AppUsageActivity.formattedTime(context, usEntry.getDuration()));
convertView.setBackgroundColor(position % 2 == 0 ? mColorSemiTransparent : mColorTransparent);
return convertView;
}
}
}
......@@ -72,7 +72,7 @@ public class EditShortcutDialogFragment extends DialogFragment {
try {
activityIconResourceName[0] = mPackageManager.getResourcesForActivity(activity).getResourceName(mActivityInfo.getIconResource());
text_icon.setText(activityIconResourceName[0]);
} catch (PackageManager.NameNotFoundException ignored) {}
} catch (PackageManager.NameNotFoundException | Resources.NotFoundException ignored) {}
text_icon.addTextChangedListener(new TextWatcher() {
@Override
......
package io.github.muntashirakon.AppManager.runner;
import android.content.Context;
import android.text.TextUtils;
import com.jaredrummler.android.shell.CommandResult;
import com.jaredrummler.android.shell.Shell;
import java.util.List;
class RootShellRunner extends Runner {
protected RootShellRunner(Context context) {
super(context);
}
@Override
public Result run() {
CommandResult result = Shell.SU.run(TextUtils.join("; ", commands));
clear();
lastResult = new Result() {
@Override
public boolean isSuccessful() {
return result.isSuccessful();
}
@Override
public List<String> getOutputAsList() {
return result.stdout;
}
@Override
public List<String> getOutputAsList(int first_index) {
return result.stdout.subList(first_index, result.stdout.size());
}
@Override
public List<String> getOutputAsList(int first_index, int length) {
return result.stdout.subList(first_index, first_index + length);
}
@Override
public String getOutput() {
return result.getStdout();
}
};
return lastResult;
}
@Override
protected Result run(String command) {
clear();
addCommand(command);
return run();
}
}
package io.github.muntashirakon.AppManager.runner;
import android.annotation.SuppressLint;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
public class Runner {
public interface Result {
boolean isSuccessful();
List<String> getOutputAsList();
List<String> getOutputAsList(int first_index);
List<String> getOutputAsList(int first_index, int length);
String getOutput();
}
@SuppressLint("StaticFieldLeak")
private static Runner runner;
public static Runner getInstance(Context context) {
// TODO: Determine class type based on preferences
if (runner == null) runner = new RootShellRunner(context.getApplicationContext());
return runner;
}
public static Result run(Context context, String command) {
return getInstance(context).run(command);
}
protected List<String> commands;
public void addCommand(String command) {
commands.add(command);
}
public void clear() {
commands.clear();
}
public Result run() {
return null;
}
protected static Result lastResult;
public static Result getLastResult() {
return lastResult;
}
protected Context context;
protected Runner(Context context) {
this.context = context;
this.commands = new ArrayList<>();
}
protected Result run(String command) {
return null;
}
}
package io.github.muntashirakon.AppManager.storage;
import android.annotation.SuppressLint;
import android.content.Context;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.muntashirakon.AppManager.appops.AppOpsManager;
public class StorageManager {
@SuppressLint("StaticFieldLeak")
private static StorageManager storageManager;
public static StorageManager getInstance(Context context, String packageName) {
if (storageManager == null) storageManager = new StorageManager(context.getApplicationContext());
storageManager.setPackageName(packageName);
return storageManager;
}
public enum Type {
ACTIVITY,
PROVIDER,
RECEIVER,
SERVICE,
APP_OP,
PERMISSION,
UNKNOWN
}
public static class Entry {
public String name; // pk
public Type type;
public Object extra; // mode, is_applied, is_granted
@NonNull
@Override
public String toString() {
return "Entry{" +
"name='" + name + '\'' +
", type=" + type +
", extra=" + extra +
'}';
}
}
private Context context;
private String packageName;
private List<Entry> entries;
private StorageManager(Context context) {
this.context = context;
}
private void setPackageName(String packageName) {
this.packageName = packageName;
loadEntries();
}
public Entry get(String name) {
for (Entry entry: entries) if (entry.name.equals(name)) return entry;
return null;
}
public List<Entry> getAll(Type type) {
List<Entry> newEntries = new ArrayList<>();
for (Entry entry: entries) if (entry.type.equals(type)) newEntries.add(entry);
return newEntries;
}
public List<Entry> getAllComponents() {
List<Entry> newEntries = new ArrayList<>();
for (Entry entry: entries) {
if (entry.type.equals(Type.ACTIVITY)
|| entry.type.equals(Type.PROVIDER)
|| entry.type.equals(Type.RECEIVER)
|| entry.type.equals(Type.SERVICE))
newEntries.add(entry);
}
return newEntries;
}
public List<Entry> getAll() {
return entries;
}
public boolean hasName(String name) {
for (Entry entry: entries) if (entry.name.equals(name)) return true;
return false;
}
public boolean hasEntry(Entry entry) {
for (Entry _entry: entries) if (_entry.equals(entry)) return true;
return false;
}
public void removeEntry(Entry entry) {
entries.remove(entry);
commit();
}
public void removeEntry(String name) {
removeEntry(name, true);
}
public void setComponent(String name, Type componentType, Boolean isApplied) {
Entry entry = new Entry();
entry.name = name;
entry.type = componentType;
entry.extra = isApplied;
addEntry(entry);
}
public void setAppOp(String name, @AppOpsManager.Mode int mode) {
Entry entry = new Entry();
entry.name = name;
entry.type = Type.APP_OP;
entry.extra = mode;
addEntry(entry);
}
public void setPermission(String name, Boolean isGranted) {
Entry entry = new Entry();
entry.name = name;
entry.type = Type.PERMISSION;
entry.extra = isGranted;
addEntry(entry);
}
private void addEntry(@NonNull Entry entry) {
removeEntry(entry.name, false);
entries.add(entry);
commit();
}
private void removeEntry(String name, Boolean isCommit) {
for (Iterator<Entry> iterator = entries.iterator(); iterator.hasNext();)
if (iterator.next().name.equals(name)) iterator.remove();
if (isCommit) commit();
}
private void loadEntries() {
try {
entries = new ArrayList<>();
StringTokenizer tokenizer;
BufferedReader TSVFile = new BufferedReader(new FileReader(getDesiredFile()));
String dataRow;
while ((dataRow = TSVFile.readLine()) != null){
tokenizer = new StringTokenizer(dataRow,"\t");
Entry entry = new Entry();
if (tokenizer.hasMoreElements()) entry.name = tokenizer.nextElement().toString();
if (tokenizer.hasMoreElements()) entry.type = getType(tokenizer.nextElement().toString());
if (tokenizer.hasMoreElements()) entry.extra = getExtra(entry.type, tokenizer.nextElement().toString());
entries.add(entry);
}
TSVFile.close();
} catch (IOException ignore) {}
}
private void commit() {
new Thread(() -> saveEntries(new ArrayList<>(entries))).start();
}
private void saveEntries(List<Entry> finalEntries) {
try {
if (finalEntries.size() == 0) {
//noinspection ResultOfMethodCallIgnored
getDesiredFile().delete();
return;
}
StringBuilder stringBuilder = new StringBuilder();
for(Entry entry: finalEntries) {
stringBuilder.append(entry.name).append("\t").append(entry.type).append("\t").append(entry.extra).append("\n");
}
FileOutputStream TSVFile = new FileOutputStream(getDesiredFile());
TSVFile.write(stringBuilder.toString().getBytes());
TSVFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@NonNull
private File getDesiredFile() throws FileNotFoundException {
File file = new File(context.getFilesDir(), "conf");
if (!file.exists() && !file.mkdirs()) {
throw new FileNotFoundException("Can not get correct path to save ifw rules");
}
return new File(file, packageName + ".tsv");
}
private Type getType(@NonNull String strType) {
switch (strType) {
case "ACTIVITY": return Type.ACTIVITY;
case "PROVIDER": return Type.PROVIDER;
case "RECEIVER": return Type.RECEIVER;
case "SERVICE": return Type.SERVICE;
case "APP_OP": return Type.APP_OP;
case "PERMISSION": return Type.PERMISSION;
default: return Type.UNKNOWN;
}
}
@Nullable
private Object getExtra(@NonNull Type type, @NonNull String strExtra) {
switch (type) {
case ACTIVITY:
case PROVIDER:
case RECEIVER:
case SERVICE:
case PERMISSION:
return Boolean.valueOf(strExtra);
case APP_OP: return Integer.valueOf(strExtra);
default: return null;
}
}
}
......@@ -10,6 +10,8 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.net.NetworkCapabilities;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import java.text.SimpleDateFormat;
......@@ -20,6 +22,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
......@@ -287,13 +290,12 @@ public class AppUsageStatsManager {
return new Tuple<>(new Tuple<>(totalWifiTx, totalWifiRx), new Tuple<>(totalMobileTx, totalMobileRx));
}
public static class PackageUS {
public static class PackageUS implements Parcelable {
public @NonNull String packageName;
public String appLabel;
public Long screenTime = 0L;
public Long lastUsageTime = 0L;
public Integer timesOpened = 0;
public Integer notificationReceived = 0;
public Tuple<Long, Long> mobileData; // Tx, Rx
public Tuple<Long, Long> wifiData; // Tx, Rx
public @Nullable List<USEntry> entries;
......@@ -302,6 +304,63 @@ public class AppUsageStatsManager {
this.packageName = packageName;
}
protected PackageUS(@NonNull Parcel in) {
packageName = Objects.requireNonNull(in.readString());
appLabel = in.readString();
screenTime = in.readByte() == 0 ? 0L : in.readLong();
lastUsageTime = in.readByte() == 0 ? 0L : in.readLong();
timesOpened = in.readByte() == 0 ? 0 : in.readInt();
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(packageName);
dest.writeString(appLabel);
if (screenTime == null) {
dest.writeByte((byte) 0);
} else {
dest.writeByte((byte) 1);
dest.writeLong(screenTime);
}
if (lastUsageTime == null) {
dest.writeByte((byte) 0);
} else {
dest.writeByte((byte) 1);
dest.writeLong(lastUsageTime);
}
if (timesOpened == null) {
dest.writeByte((byte) 0);
} else {
dest.writeByte((byte) 1);
dest.writeInt(timesOpened);
}
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<PackageUS> CREATOR = new Creator<PackageUS>() {
@Override
public PackageUS createFromParcel(Parcel in) {
return new PackageUS(in);
}
@Override
public PackageUS[] newArray(int size) {
return new PackageUS[size];
}
};
public void copyOthers(@NonNull PackageUS packageUS) {
screenTime = packageUS.screenTime;
lastUsageTime = packageUS.lastUsageTime;
timesOpened = packageUS.timesOpened;
mobileData = packageUS.mobileData;
wifiData = packageUS.wifiData;
}
@NonNull
@Override
public String toString() {
......@@ -311,7 +370,6 @@ public class AppUsageStatsManager {
", screenTime=" + screenTime +
", lastUsageTime=" + lastUsageTime +
", timesOpened=" + timesOpened +
", notificationReceived=" + notificationReceived +
", txData=" + mobileData +
", rxData=" + wifiData +
", entries=" + entries +
......
......@@ -25,8 +25,6 @@ import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.WindowManager;
import com.jaredrummler.android.shell.Shell;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
......@@ -60,6 +58,7 @@ import javax.xml.xpath.XPathFactory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.muntashirakon.AppManager.runner.Runner;
@SuppressWarnings("unused")
public class Utils {
......@@ -588,9 +587,9 @@ public class Utils {
return typedValue.data;
}
public static boolean isRootGiven() {
public static boolean isRootGiven(Context context) {
if (isRootAvailable()) {
String output = Shell.SU.run("id").getStdout();
String output = Runner.run(context, "id").getOutput();
return output != null && output.toLowerCase().contains("uid=0");
}
return false;
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorAccent"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progress_horizontal"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:paddingStart="@dimen/padding_medium"
android:paddingEnd="@dimen/padding_medium" />
<include layout="@layout/appbar" />
<ScrollView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
android:orientation="vertical"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<HorizontalScrollView
<ProgressBar
android:id="@+id/progress_horizontal"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:paddingStart="@dimen/padding_medium"
android:paddingEnd="@dimen/padding_medium" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<TextView
android:id="@+id/any_view"
android:layout_width="wrap_content"
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:gravity="top"
android:padding="@dimen/padding_small"
android:textIsSelectable="true"
tools:text="@tools:sample/lorem/random" />
</HorizontalScrollView>
</ScrollView>
</LinearLayout>
\ No newline at end of file
android:fillViewport="true">
<TextView
android:id="@+id/any_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:gravity="top"
android:padding="@dimen/padding_small"
android:textIsSelectable="true"
tools:text="@tools:sample/lorem/random" />
</HorizontalScrollView>
</ScrollView>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progress_horizontal"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:paddingStart="@dimen/padding_medium"
android:paddingEnd="@dimen/padding_medium" />
<include layout="@layout/appbar" />
<ScrollView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
android:orientation="vertical"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<TextView
android:id="@+id/any_view"
android:layout_width="wrap_content"
<ProgressBar
android:id="@+id/progress_horizontal"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:gravity="top"
android:padding="@dimen/padding_small"
android:textIsSelectable="true"
tools:text="@tools:sample/lorem/random" />
</ScrollView>
</LinearLayout>
\ No newline at end of file
android:indeterminate="true"
android:paddingStart="@dimen/padding_medium"
android:paddingEnd="@dimen/padding_medium" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<TextView
android:id="@+id/any_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:gravity="top"
android:padding="@dimen/padding_small"
android:textIsSelectable="true"
tools:text="@tools:sample/lorem/random" />
</ScrollView>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="io.github.muntashirakon.AppManager.activities.AppDetailsActivity">
tools:context=".activities.AppDetailsActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"