blob: 21e4165e6a674e2ba608209b856cb687c665dc88 [file] [log] [blame]
/*
* Copyright (C) 2016 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.server.pm;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.pm.ShortcutManager;
import android.text.TextUtils;
import android.text.format.Formatter;
import android.util.ArrayMap;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import libcore.util.Objects;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.function.Consumer;
/**
* User information used by {@link ShortcutService}.
*
* All methods should be guarded by {@code #mService.mLock}.
*/
class ShortcutUser {
private static final String TAG = ShortcutService.TAG;
static final String TAG_ROOT = "user";
private static final String TAG_LAUNCHER = "launcher";
private static final String ATTR_VALUE = "value";
private static final String ATTR_KNOWN_LOCALES = "locales";
private static final String ATTR_LAST_APP_SCAN_TIME = "last-app-scan-time";
private static final String KEY_USER_ID = "userId";
private static final String KEY_LAUNCHERS = "launchers";
private static final String KEY_PACKAGES = "packages";
static final class PackageWithUser {
final int userId;
final String packageName;
private PackageWithUser(int userId, String packageName) {
this.userId = userId;
this.packageName = Preconditions.checkNotNull(packageName);
}
public static PackageWithUser of(int userId, String packageName) {
return new PackageWithUser(userId, packageName);
}
public static PackageWithUser of(ShortcutPackageItem spi) {
return new PackageWithUser(spi.getPackageUserId(), spi.getPackageName());
}
@Override
public int hashCode() {
return packageName.hashCode() ^ userId;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PackageWithUser)) {
return false;
}
final PackageWithUser that = (PackageWithUser) obj;
return userId == that.userId && packageName.equals(that.packageName);
}
@Override
public String toString() {
return String.format("[Package: %d, %s]", userId, packageName);
}
}
final ShortcutService mService;
@UserIdInt
private final int mUserId;
private final ArrayMap<String, ShortcutPackage> mPackages = new ArrayMap<>();
private final ArrayMap<PackageWithUser, ShortcutLauncher> mLaunchers = new ArrayMap<>();
/**
* Last known launcher. It's used when the default launcher isn't set in PM -- i.e.
* when getHomeActivitiesAsUser() return null. We need it so that in this situation the
* previously default launcher can still access shortcuts.
*/
private ComponentName mLastKnownLauncher;
/** In-memory-cached default launcher. */
private ComponentName mCachedLauncher;
private String mKnownLocales;
private long mLastAppScanTime;
public ShortcutUser(ShortcutService service, int userId) {
mService = service;
mUserId = userId;
}
public int getUserId() {
return mUserId;
}
public long getLastAppScanTime() {
return mLastAppScanTime;
}
public void setLastAppScanTime(long lastAppScanTime) {
mLastAppScanTime = lastAppScanTime;
}
// We don't expose this directly to non-test code because only ShortcutUser should add to/
// remove from it.
@VisibleForTesting
ArrayMap<String, ShortcutPackage> getAllPackagesForTest() {
return mPackages;
}
public boolean hasPackage(@NonNull String packageName) {
return mPackages.containsKey(packageName);
}
public ShortcutPackage removePackage(@NonNull String packageName) {
final ShortcutPackage removed = mPackages.remove(packageName);
mService.cleanupBitmapsForPackage(mUserId, packageName);
return removed;
}
// We don't expose this directly to non-test code because only ShortcutUser should add to/
// remove from it.
@VisibleForTesting
ArrayMap<PackageWithUser, ShortcutLauncher> getAllLaunchersForTest() {
return mLaunchers;
}
public void addLauncher(ShortcutLauncher launcher) {
mLaunchers.put(PackageWithUser.of(launcher.getPackageUserId(),
launcher.getPackageName()), launcher);
}
@Nullable
public ShortcutLauncher removeLauncher(
@UserIdInt int packageUserId, @NonNull String packageName) {
return mLaunchers.remove(PackageWithUser.of(packageUserId, packageName));
}
@Nullable
public ShortcutPackage getPackageShortcutsIfExists(@NonNull String packageName) {
final ShortcutPackage ret = mPackages.get(packageName);
if (ret != null) {
ret.attemptToRestoreIfNeededAndSave();
}
return ret;
}
@NonNull
public ShortcutPackage getPackageShortcuts(@NonNull String packageName) {
ShortcutPackage ret = getPackageShortcutsIfExists(packageName);
if (ret == null) {
ret = new ShortcutPackage(this, mUserId, packageName);
mPackages.put(packageName, ret);
}
return ret;
}
@NonNull
public ShortcutLauncher getLauncherShortcuts(@NonNull String packageName,
@UserIdInt int launcherUserId) {
final PackageWithUser key = PackageWithUser.of(launcherUserId, packageName);
ShortcutLauncher ret = mLaunchers.get(key);
if (ret == null) {
ret = new ShortcutLauncher(this, mUserId, packageName, launcherUserId);
mLaunchers.put(key, ret);
} else {
ret.attemptToRestoreIfNeededAndSave();
}
return ret;
}
public void forAllPackages(Consumer<? super ShortcutPackage> callback) {
final int size = mPackages.size();
for (int i = 0; i < size; i++) {
callback.accept(mPackages.valueAt(i));
}
}
public void forAllLaunchers(Consumer<? super ShortcutLauncher> callback) {
final int size = mLaunchers.size();
for (int i = 0; i < size; i++) {
callback.accept(mLaunchers.valueAt(i));
}
}
public void forAllPackageItems(Consumer<? super ShortcutPackageItem> callback) {
forAllLaunchers(callback);
forAllPackages(callback);
}
public void forPackageItem(@NonNull String packageName, @UserIdInt int packageUserId,
Consumer<ShortcutPackageItem> callback) {
forAllPackageItems(spi -> {
if ((spi.getPackageUserId() == packageUserId)
&& spi.getPackageName().equals(packageName)) {
callback.accept(spi);
}
});
}
/**
* Must be called at any entry points on {@link ShortcutManager} APIs to make sure the
* information on the package is up-to-date.
*
* We use broadcasts to handle locale changes and package changes, but because broadcasts
* are asynchronous, there's a chance a publisher calls getXxxShortcuts() after a certain event
* (e.g. system locale change) but shortcut manager hasn't finished processing the broadcast.
*
* So we call this method at all entry points from publishers to make sure we update all
* relevant information.
*
* Similar inconsistencies can happen when the launcher fetches shortcut information, but
* that's a less of an issue because for the launcher we report shortcut changes with
* callbacks.
*/
public void onCalledByPublisher(@NonNull String packageName) {
detectLocaleChange();
rescanPackageIfNeeded(packageName, /*forceRescan=*/ false);
}
private String getKnownLocales() {
if (TextUtils.isEmpty(mKnownLocales)) {
mKnownLocales = mService.injectGetLocaleTagsForUser(mUserId);
mService.scheduleSaveUser(mUserId);
}
return mKnownLocales;
}
/**
* Check to see if the system locale has changed, and if so, reset throttling
* and update resource strings.
*/
public void detectLocaleChange() {
final String currentLocales = mService.injectGetLocaleTagsForUser(mUserId);
if (getKnownLocales().equals(currentLocales)) {
return;
}
if (ShortcutService.DEBUG) {
Slog.d(TAG, "Locale changed from " + currentLocales + " to " + mKnownLocales
+ " for user " + mUserId);
}
mKnownLocales = currentLocales;
forAllPackages(pkg -> {
pkg.resetRateLimiting();
pkg.resolveResourceStrings();
});
mService.scheduleSaveUser(mUserId);
}
public void rescanPackageIfNeeded(@NonNull String packageName, boolean forceRescan) {
final boolean isNewApp = !mPackages.containsKey(packageName);
final ShortcutPackage shortcutPackage = getPackageShortcuts(packageName);
if (!shortcutPackage.rescanPackageIfNeeded(isNewApp, forceRescan)) {
if (isNewApp) {
mPackages.remove(packageName);
}
}
}
public void attemptToRestoreIfNeededAndSave(ShortcutService s, @NonNull String packageName,
@UserIdInt int packageUserId) {
forPackageItem(packageName, packageUserId, spi -> {
spi.attemptToRestoreIfNeededAndSave();
});
}
public void saveToXml(XmlSerializer out, boolean forBackup)
throws IOException, XmlPullParserException {
out.startTag(null, TAG_ROOT);
ShortcutService.writeAttr(out, ATTR_KNOWN_LOCALES, mKnownLocales);
ShortcutService.writeAttr(out, ATTR_LAST_APP_SCAN_TIME,
mLastAppScanTime);
ShortcutService.writeTagValue(out, TAG_LAUNCHER, mLastKnownLauncher);
// Can't use forEachPackageItem due to the checked exceptions.
{
final int size = mLaunchers.size();
for (int i = 0; i < size; i++) {
saveShortcutPackageItem(out, mLaunchers.valueAt(i), forBackup);
}
}
{
final int size = mPackages.size();
for (int i = 0; i < size; i++) {
saveShortcutPackageItem(out, mPackages.valueAt(i), forBackup);
}
}
out.endTag(null, TAG_ROOT);
}
private void saveShortcutPackageItem(XmlSerializer out,
ShortcutPackageItem spi, boolean forBackup) throws IOException, XmlPullParserException {
if (forBackup) {
if (!mService.shouldBackupApp(spi.getPackageName(), spi.getPackageUserId())) {
return; // Don't save.
}
if (spi.getPackageUserId() != spi.getOwnerUserId()) {
return; // Don't save cross-user information.
}
}
spi.saveToXml(out, forBackup);
}
public static ShortcutUser loadFromXml(ShortcutService s, XmlPullParser parser, int userId,
boolean fromBackup) throws IOException, XmlPullParserException {
final ShortcutUser ret = new ShortcutUser(s, userId);
ret.mKnownLocales = ShortcutService.parseStringAttribute(parser,
ATTR_KNOWN_LOCALES);
// If lastAppScanTime is in the future, that means the clock went backwards.
// Just scan all apps again.
final long lastAppScanTime = ShortcutService.parseLongAttribute(parser,
ATTR_LAST_APP_SCAN_TIME);
final long currentTime = s.injectCurrentTimeMillis();
ret.mLastAppScanTime = lastAppScanTime < currentTime ? lastAppScanTime : 0;
final int outerDepth = parser.getDepth();
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final int depth = parser.getDepth();
final String tag = parser.getName();
if (depth == outerDepth + 1) {
switch (tag) {
case TAG_LAUNCHER: {
ret.mLastKnownLauncher = ShortcutService.parseComponentNameAttribute(
parser, ATTR_VALUE);
continue;
}
case ShortcutPackage.TAG_ROOT: {
final ShortcutPackage shortcuts = ShortcutPackage.loadFromXml(
s, ret, parser, fromBackup);
// Don't use addShortcut(), we don't need to save the icon.
ret.mPackages.put(shortcuts.getPackageName(), shortcuts);
continue;
}
case ShortcutLauncher.TAG_ROOT: {
ret.addLauncher(
ShortcutLauncher.loadFromXml(parser, ret, userId, fromBackup));
continue;
}
}
}
ShortcutService.warnForInvalidTag(depth, tag);
}
return ret;
}
public ComponentName getLastKnownLauncher() {
return mLastKnownLauncher;
}
public void setLauncher(ComponentName launcherComponent) {
setLauncher(launcherComponent, /* allowPurgeLastKnown */ false);
}
/** Clears the launcher information without clearing the last known one */
public void clearLauncher() {
setLauncher(null);
}
/**
* Clears the launcher information *with(* clearing the last known one; we do this witl
* "cmd shortcut clear-default-launcher".
*/
public void forceClearLauncher() {
setLauncher(null, /* allowPurgeLastKnown */ true);
}
private void setLauncher(ComponentName launcherComponent, boolean allowPurgeLastKnown) {
mCachedLauncher = launcherComponent; // Always update the in-memory cache.
if (Objects.equal(mLastKnownLauncher, launcherComponent)) {
return;
}
if (!allowPurgeLastKnown && launcherComponent == null) {
return;
}
mLastKnownLauncher = launcherComponent;
mService.scheduleSaveUser(mUserId);
}
public ComponentName getCachedLauncher() {
return mCachedLauncher;
}
public void resetThrottling() {
for (int i = mPackages.size() - 1; i >= 0; i--) {
mPackages.valueAt(i).resetThrottling();
}
}
public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
pw.print(prefix);
pw.print("User: ");
pw.print(mUserId);
pw.print(" Known locales: ");
pw.print(mKnownLocales);
pw.print(" Last app scan: [");
pw.print(mLastAppScanTime);
pw.print("] ");
pw.print(ShortcutService.formatTime(mLastAppScanTime));
pw.println();
prefix += prefix + " ";
pw.print(prefix);
pw.print("Cached launcher: ");
pw.print(mCachedLauncher);
pw.println();
pw.print(prefix);
pw.print("Last known launcher: ");
pw.print(mLastKnownLauncher);
pw.println();
for (int i = 0; i < mLaunchers.size(); i++) {
mLaunchers.valueAt(i).dump(pw, prefix);
}
for (int i = 0; i < mPackages.size(); i++) {
mPackages.valueAt(i).dump(pw, prefix);
}
pw.println();
pw.print(prefix);
pw.println("Bitmap directories: ");
dumpDirectorySize(pw, prefix + " ", mService.getUserBitmapFilePath(mUserId));
}
private void dumpDirectorySize(@NonNull PrintWriter pw,
@NonNull String prefix, File path) {
int numFiles = 0;
long size = 0;
final File[] children = path.listFiles();
if (children != null) {
for (File child : path.listFiles()) {
if (child.isFile()) {
numFiles++;
size += child.length();
} else if (child.isDirectory()) {
dumpDirectorySize(pw, prefix + " ", child);
}
}
}
pw.print(prefix);
pw.print("Path: ");
pw.print(path.getName());
pw.print("/ has ");
pw.print(numFiles);
pw.print(" files, size=");
pw.print(size);
pw.print(" (");
pw.print(Formatter.formatFileSize(mService.mContext, size));
pw.println(")");
}
public JSONObject dumpCheckin(boolean clear) throws JSONException {
final JSONObject result = new JSONObject();
result.put(KEY_USER_ID, mUserId);
{
final JSONArray launchers = new JSONArray();
for (int i = 0; i < mLaunchers.size(); i++) {
launchers.put(mLaunchers.valueAt(i).dumpCheckin(clear));
}
result.put(KEY_LAUNCHERS, launchers);
}
{
final JSONArray packages = new JSONArray();
for (int i = 0; i < mPackages.size(); i++) {
packages.put(mPackages.valueAt(i).dumpCheckin(clear));
}
result.put(KEY_PACKAGES, packages);
}
return result;
}
}