blob: 2a3e5eb9672f3831e4fd8c6ab2eab79c6930d47c [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tv.settings.device.apps;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import com.android.internal.logging.nano.MetricsProto;
import com.android.settingslib.applications.ApplicationsState;
import com.android.tv.settings.R;
import com.android.tv.settings.SettingsPreferenceFragment;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
/**
* Fragment for listing and managing all apps on the device.
*/
public class AllAppsFragment extends SettingsPreferenceFragment implements
Preference.OnPreferenceClickListener {
private static final String TAG = "AllAppsFragment";
private static final String KEY_SHOW_OTHER_APPS = "ShowOtherApps";
private static final @ApplicationsState.SessionFlags int SESSION_FLAGS =
ApplicationsState.FLAG_SESSION_REQUEST_HOME_APP
| ApplicationsState.FLAG_SESSION_REQUEST_ICONS
| ApplicationsState.FLAG_SESSION_REQUEST_SIZES
| ApplicationsState.FLAG_SESSION_REQUEST_LEANBACK_LAUNCHER;
private ApplicationsState mApplicationsState;
private ApplicationsState.Session mSessionInstalled;
private ApplicationsState.AppFilter mFilterInstalled;
private ApplicationsState.Session mSessionDisabled;
private ApplicationsState.AppFilter mFilterDisabled;
private ApplicationsState.Session mSessionOther;
private ApplicationsState.AppFilter mFilterOther;
private PreferenceGroup mInstalledPreferenceGroup;
private PreferenceGroup mDisabledPreferenceGroup;
private PreferenceGroup mOtherPreferenceGroup;
private Preference mShowOtherApps;
private final Handler mHandler = new Handler();
private final Map<PreferenceGroup,
ArrayList<ApplicationsState.AppEntry>> mUpdateMap = new ArrayMap<>(3);
private long mRunAt = Long.MIN_VALUE;
private final Runnable mUpdateRunnable = new Runnable() {
@Override
public void run() {
for (final PreferenceGroup group : mUpdateMap.keySet()) {
final ArrayList<ApplicationsState.AppEntry> entries = mUpdateMap.get(group);
updateAppListInternal(group, entries);
}
mUpdateMap.clear();
mRunAt = 0;
}
};
/** Prepares arguments for the fragment. */
public static void prepareArgs(Bundle b, String volumeUuid, String volumeName) {
b.putString(AppsActivity.EXTRA_VOLUME_UUID, volumeUuid);
b.putString(AppsActivity.EXTRA_VOLUME_NAME, volumeName);
}
/** Creates a new instance of the fragment. */
public static AllAppsFragment newInstance(String volumeUuid, String volumeName) {
final Bundle b = new Bundle(2);
prepareArgs(b, volumeUuid, volumeName);
final AllAppsFragment f = new AllAppsFragment();
f.setArguments(b);
return f;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mApplicationsState = ApplicationsState.getInstance(getActivity().getApplication());
final String volumeUuid = getArguments().getString(AppsActivity.EXTRA_VOLUME_UUID);
final String volumeName = getArguments().getString(AppsActivity.EXTRA_VOLUME_NAME);
// The UUID of internal storage is null, so we check if there's a volume name to see if we
// should only be showing the apps on the internal storage or all apps.
if (!TextUtils.isEmpty(volumeUuid) || !TextUtils.isEmpty(volumeName)) {
ApplicationsState.AppFilter volumeFilter =
new ApplicationsState.VolumeFilter(volumeUuid);
mFilterInstalled =
new ApplicationsState.CompoundFilter(FILTER_INSTALLED, volumeFilter);
mFilterDisabled =
new ApplicationsState.CompoundFilter(FILTER_DISABLED, volumeFilter);
mFilterOther =
new ApplicationsState.CompoundFilter(FILTER_OTHER, volumeFilter);
} else {
mFilterInstalled = FILTER_INSTALLED;
mFilterDisabled = FILTER_DISABLED;
mFilterOther = FILTER_OTHER;
}
mSessionInstalled = mApplicationsState.newSession(new RowUpdateCallbacks() {
@Override
protected void doRebuild() {
rebuildInstalled();
}
@Override
public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
updateAppList(mInstalledPreferenceGroup, apps);
}
}, getLifecycle());
mSessionInstalled.setSessionFlags(SESSION_FLAGS);
mSessionDisabled = mApplicationsState.newSession(new RowUpdateCallbacks() {
@Override
protected void doRebuild() {
rebuildDisabled();
}
@Override
public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
updateAppList(mDisabledPreferenceGroup, apps);
}
}, getLifecycle());
mSessionDisabled.setSessionFlags(SESSION_FLAGS);
mSessionOther = mApplicationsState.newSession(new RowUpdateCallbacks() {
@Override
protected void doRebuild() {
if (!mShowOtherApps.isVisible()) {
rebuildOther();
}
}
@Override
public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {
updateAppList(mOtherPreferenceGroup, apps);
}
}, getLifecycle());
mSessionOther.setSessionFlags(SESSION_FLAGS);
rebuildInstalled();
rebuildDisabled();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.all_apps, null);
mInstalledPreferenceGroup = (PreferenceGroup) findPreference("InstalledPreferenceGroup");
mDisabledPreferenceGroup = (PreferenceGroup) findPreference("DisabledPreferenceGroup");
mOtherPreferenceGroup = (PreferenceGroup) findPreference("OtherPreferenceGroup");
mOtherPreferenceGroup.setVisible(false);
mShowOtherApps = findPreference(KEY_SHOW_OTHER_APPS);
mShowOtherApps.setOnPreferenceClickListener(this);
final String volumeUuid = getArguments().getString(AppsActivity.EXTRA_VOLUME_UUID);
mShowOtherApps.setVisible(TextUtils.isEmpty(volumeUuid));
}
private void rebuildInstalled() {
ArrayList<ApplicationsState.AppEntry> apps =
mSessionInstalled.rebuild(mFilterInstalled, ApplicationsState.ALPHA_COMPARATOR);
if (apps != null) {
updateAppList(mInstalledPreferenceGroup, apps);
}
}
private void rebuildDisabled() {
ArrayList<ApplicationsState.AppEntry> apps =
mSessionDisabled.rebuild(mFilterDisabled, ApplicationsState.ALPHA_COMPARATOR);
if (apps != null) {
updateAppList(mDisabledPreferenceGroup, apps);
}
}
private void rebuildOther() {
ArrayList<ApplicationsState.AppEntry> apps =
mSessionOther.rebuild(mFilterOther, ApplicationsState.ALPHA_COMPARATOR);
if (apps != null) {
updateAppList(mOtherPreferenceGroup, apps);
}
}
private void updateAppList(PreferenceGroup group,
ArrayList<ApplicationsState.AppEntry> entries) {
if (group == null) {
Log.d(TAG, "Not updating list for null group");
return;
}
mUpdateMap.put(group, entries);
// We can get spammed with updates, so coalesce them to reduce jank and flicker
if (mRunAt == Long.MIN_VALUE) {
// First run, no delay
mHandler.removeCallbacks(mUpdateRunnable);
mHandler.post(mUpdateRunnable);
} else {
if (mRunAt == 0) {
mRunAt = SystemClock.uptimeMillis() + 1000;
}
int delay = (int) (mRunAt - SystemClock.uptimeMillis());
delay = delay < 0 ? 0 : delay;
mHandler.removeCallbacks(mUpdateRunnable);
mHandler.postDelayed(mUpdateRunnable, delay);
}
}
private void updateAppListInternal(PreferenceGroup group,
ArrayList<ApplicationsState.AppEntry> entries) {
if (entries != null) {
final Set<String> touched = new ArraySet<>(entries.size());
for (final ApplicationsState.AppEntry entry : entries) {
final String packageName = entry.info.packageName;
Preference recycle = group.findPreference(packageName);
if (recycle == null) {
recycle = new Preference(getPreferenceManager().getContext());
}
final Preference newPref = bindPreference(recycle, entry);
group.addPreference(newPref);
touched.add(packageName);
}
for (int i = 0; i < group.getPreferenceCount();) {
final Preference pref = group.getPreference(i);
if (touched.contains(pref.getKey())) {
i++;
} else {
group.removePreference(pref);
}
}
}
mDisabledPreferenceGroup.setVisible(mDisabledPreferenceGroup.getPreferenceCount() > 0);
}
/**
* Creates or updates a preference according to an {@link ApplicationsState.AppEntry} object
* @param preference If non-null, updates this preference object, otherwise creates a new one
* @param entry Info to populate preference
* @return Updated preference entry
*/
private Preference bindPreference(@NonNull Preference preference,
ApplicationsState.AppEntry entry) {
preference.setKey(entry.info.packageName);
entry.ensureLabel(getContext());
preference.setTitle(entry.label);
preference.setSummary(entry.sizeStr);
preference.setFragment(AppManagementFragment.class.getName());
AppManagementFragment.prepareArgs(preference.getExtras(), entry.info.packageName);
preference.setIcon(entry.icon);
return preference;
}
@Override
public boolean onPreferenceClick(Preference preference) {
if (KEY_SHOW_OTHER_APPS.equals(preference.getKey())) {
showOtherApps();
return true;
}
return false;
}
private void showOtherApps() {
mShowOtherApps.setVisible(false);
mOtherPreferenceGroup.setVisible(true);
rebuildOther();
}
private abstract class RowUpdateCallbacks implements ApplicationsState.Callbacks {
protected abstract void doRebuild();
@Override
public void onRunningStateChanged(boolean running) {
doRebuild();
}
@Override
public void onPackageListChanged() {
doRebuild();
}
@Override
public void onPackageIconChanged() {
doRebuild();
}
@Override
public void onPackageSizeChanged(String packageName) {
doRebuild();
}
@Override
public void onAllSizesComputed() {
doRebuild();
}
@Override
public void onLauncherInfoChanged() {
doRebuild();
}
@Override
public void onLoadEntriesCompleted() {
doRebuild();
}
}
private static final ApplicationsState.AppFilter FILTER_INSTALLED =
new ApplicationsState.AppFilter() {
@Override
public void init() {}
@Override
public boolean filterApp(ApplicationsState.AppEntry info) {
return !FILTER_DISABLED.filterApp(info)
&& info.info != null
&& info.info.enabled
&& info.hasLauncherEntry
&& info.launcherEntryEnabled;
}
};
private static final ApplicationsState.AppFilter FILTER_DISABLED =
new ApplicationsState.AppFilter() {
@Override
public void init() {
}
@Override
public boolean filterApp(ApplicationsState.AppEntry info) {
return info.info != null
&& (info.info.enabledSetting
== PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|| info.info.enabledSetting
== PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
|| (info.info.enabledSetting
== PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
&& !info.info.enabled));
}
};
private static final ApplicationsState.AppFilter FILTER_OTHER =
new ApplicationsState.AppFilter() {
@Override
public void init() {}
@Override
public boolean filterApp(ApplicationsState.AppEntry info) {
return !FILTER_INSTALLED.filterApp(info) && !FILTER_DISABLED.filterApp(info);
}
};
@Override
public int getMetricsCategory() {
return MetricsProto.MetricsEvent.MANAGE_APPLICATIONS;
}
}