| /* |
| * Copyright 2014, 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.managedprovisioning.task; |
| |
| import android.app.AppGlobals; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.ComponentInfo; |
| import android.content.pm.IPackageDeleteObserver; |
| import android.content.pm.IPackageManager; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.content.res.Resources; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.util.Xml; |
| import android.view.inputmethod.InputMethodInfo; |
| import android.view.inputmethod.InputMethodManager; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.FastXmlSerializer; |
| import com.android.internal.view.IInputMethodManager; |
| import com.android.managedprovisioning.ProvisionLogger; |
| import com.android.managedprovisioning.R; |
| import com.android.managedprovisioning.common.Utils; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| import org.xmlpull.v1.XmlSerializer; |
| |
| /** |
| * Deletes all system apps with a launcher that are not in the required set of packages. |
| * Furthermore deletes all disallowed apps. |
| * |
| * Note: If an app is mistakenly listed as both required and disallowed, it will be treated as |
| * required. |
| * |
| * This task may be run when a profile (both for managed device and managed profile) is created. |
| * In that case the newProfile flag should be true. |
| * |
| * It should also be run after a system update with newProfile false, if |
| * {@link #shouldDeleteNonRequiredApps} returns true. Note that only newly installed system apps |
| * will be deleted. |
| */ |
| public class DeleteNonRequiredAppsTask { |
| private final Callback mCallback; |
| private final Context mContext; |
| private final String mMdmPackageName; |
| private final IPackageManager mIPackageManager; |
| private final IInputMethodManager mIInputMethodManager; |
| private final PackageManager mPm; |
| private final List<String> mRequiredAppsList; |
| private final List<String> mDisallowedAppsList; |
| private final List<String> mVendorRequiredAppsList; |
| private final List<String> mVendorDisallowedAppsList; |
| private final int mUserId; |
| private final int mProvisioningType; |
| private final boolean mNewProfile; // If we are provisioning a new managed profile/device. |
| private final boolean mLeaveAllSystemAppsEnabled; |
| |
| private static final String TAG_SYSTEM_APPS = "system-apps"; |
| private static final String TAG_PACKAGE_LIST_ITEM = "item"; |
| private static final String ATTR_VALUE = "value"; |
| |
| public static final int DEVICE_OWNER = 0; |
| public static final int PROFILE_OWNER = 1; |
| public static final int MANAGED_USER = 2; |
| |
| private final Utils mUtils = new Utils(); |
| |
| /** |
| * Provisioning type should be either {@link #DEVICE_OWNER}, {@link #PROFILE_OWNER} or |
| * {@link #MANAGED_USER}. |
| **/ |
| public DeleteNonRequiredAppsTask(Context context, String mdmPackageName, int provisioningType, |
| boolean newProfile, int userId, boolean leaveAllSystemAppsEnabled, Callback callback) { |
| this(context, AppGlobals.getPackageManager(), getIInputMethodManager(), mdmPackageName, |
| provisioningType, newProfile, userId, leaveAllSystemAppsEnabled, callback); |
| } |
| |
| @VisibleForTesting |
| DeleteNonRequiredAppsTask(Context context, IPackageManager iPm, IInputMethodManager iimm, |
| String mdmPackageName, int provisioningType, boolean newProfile, int userId, |
| boolean leaveAllSystemAppsEnabled, Callback callback) { |
| |
| mCallback = callback; |
| mContext = context; |
| mMdmPackageName = mdmPackageName; |
| mProvisioningType = provisioningType; |
| mUserId = userId; |
| mNewProfile = newProfile; |
| mLeaveAllSystemAppsEnabled = leaveAllSystemAppsEnabled; |
| mPm = context.getPackageManager(); |
| mIPackageManager = iPm; |
| mIInputMethodManager = iimm; |
| |
| int requiredAppsListArray; |
| int vendorRequiredAppsListArray; |
| int disallowedAppsListArray; |
| int vendorDisallowedAppsListArray; |
| if (mProvisioningType == DEVICE_OWNER) { |
| requiredAppsListArray = R.array.required_apps_managed_device; |
| disallowedAppsListArray = R.array.disallowed_apps_managed_device; |
| vendorRequiredAppsListArray = R.array.vendor_required_apps_managed_device; |
| vendorDisallowedAppsListArray = R.array.vendor_disallowed_apps_managed_device; |
| } else if (mProvisioningType == PROFILE_OWNER) { |
| requiredAppsListArray = R.array.required_apps_managed_profile; |
| disallowedAppsListArray = R.array.disallowed_apps_managed_profile; |
| vendorRequiredAppsListArray = R.array.vendor_required_apps_managed_profile; |
| vendorDisallowedAppsListArray = R.array.vendor_disallowed_apps_managed_profile; |
| } else if (mProvisioningType == MANAGED_USER) { |
| requiredAppsListArray = R.array.required_apps_managed_user; |
| disallowedAppsListArray = R.array.disallowed_apps_managed_user; |
| vendorRequiredAppsListArray = R.array.vendor_required_apps_managed_user; |
| vendorDisallowedAppsListArray = R.array.vendor_disallowed_apps_managed_user; |
| } else { |
| throw new IllegalArgumentException("Provisioning type " + mProvisioningType + |
| " not supported."); |
| } |
| |
| Resources resources = mContext.getResources(); |
| mRequiredAppsList = Arrays.asList(resources.getStringArray(requiredAppsListArray)); |
| mDisallowedAppsList = Arrays.asList(resources.getStringArray(disallowedAppsListArray)); |
| mVendorRequiredAppsList = Arrays.asList( |
| resources.getStringArray(vendorRequiredAppsListArray)); |
| mVendorDisallowedAppsList = Arrays.asList( |
| resources.getStringArray(vendorDisallowedAppsListArray)); |
| } |
| |
| public void run() { |
| if (mLeaveAllSystemAppsEnabled) { |
| ProvisionLogger.logd("Not deleting non-required apps."); |
| mCallback.onSuccess(); |
| return; |
| } |
| ProvisionLogger.logd("Deleting non required apps."); |
| |
| Set<String> packagesToDelete = getPackagesToDelete(); |
| removeNonInstalledPackages(packagesToDelete); |
| |
| if (packagesToDelete.isEmpty()) { |
| mCallback.onSuccess(); |
| return; |
| } |
| |
| PackageDeleteObserver packageDeleteObserver = |
| new PackageDeleteObserver(packagesToDelete.size()); |
| for (String packageName : packagesToDelete) { |
| ProvisionLogger.logd("Deleting package [" + packageName + "] as user " + mUserId); |
| mPm.deletePackageAsUser(packageName, packageDeleteObserver, |
| PackageManager.DELETE_SYSTEM_APP, mUserId); |
| } |
| } |
| |
| private Set<String> getPackagesToDelete() { |
| Set<String> packagesToDelete = getCurrentAppsWithLauncher(); |
| // Newly installed system apps are uninstalled when they are not required and are either |
| // disallowed or have a launcher icon. |
| packagesToDelete.removeAll(getRequiredApps()); |
| // Don't delete the system input method packages in case of Device owner provisioning. |
| if (mProvisioningType == DEVICE_OWNER || mProvisioningType == MANAGED_USER) { |
| packagesToDelete.removeAll(getSystemInputMethods()); |
| } |
| packagesToDelete.addAll(getDisallowedApps()); |
| |
| // Only consider new system apps. |
| packagesToDelete.retainAll(getNewSystemApps()); |
| return packagesToDelete; |
| } |
| |
| private Set<String> getNewSystemApps() { |
| File systemAppsFile = getSystemAppsFile(mContext, mUserId); |
| systemAppsFile.getParentFile().mkdirs(); // Creating the folder if it does not exist |
| |
| Set<String> currentSystemApps = mUtils.getCurrentSystemApps(mIPackageManager, mUserId); |
| final Set<String> previousSystemApps; |
| if (mNewProfile) { |
| // Provisioning case. |
| previousSystemApps = Collections.<String>emptySet(); |
| } else if (!systemAppsFile.exists()) { |
| // OTA case. |
| ProvisionLogger.loge("Could not find the system apps file " + |
| systemAppsFile.getAbsolutePath()); |
| mCallback.onError(); |
| return Collections.<String>emptySet(); |
| } else { |
| previousSystemApps = readSystemApps(systemAppsFile); |
| } |
| |
| writeSystemApps(currentSystemApps, systemAppsFile); |
| Set<String> newApps = currentSystemApps; |
| newApps.removeAll(previousSystemApps); |
| return newApps; |
| } |
| |
| /** |
| * Remove all packages from the set that are not installed. |
| */ |
| private void removeNonInstalledPackages(Set<String> packages) { |
| Set<String> toBeRemoved = new HashSet<String>(); |
| for (String packageName : packages) { |
| try { |
| PackageInfo info = mPm.getPackageInfoAsUser(packageName, 0 /* default flags */, |
| mUserId); |
| if (info == null) { |
| toBeRemoved.add(packageName); |
| } |
| } catch (PackageManager.NameNotFoundException e) { |
| toBeRemoved.add(packageName); |
| } |
| } |
| packages.removeAll(toBeRemoved); |
| } |
| |
| /** |
| * Returns if this task should be run on OTA. |
| * This is indicated by the presence of the system apps file. |
| */ |
| public static boolean shouldDeleteNonRequiredApps(Context context, int userId) { |
| return getSystemAppsFile(context, userId).exists(); |
| } |
| |
| static File getSystemAppsFile(Context context, int userId) { |
| return new File(context.getFilesDir() + File.separator + "system_apps" |
| + File.separator + "user" + userId + ".xml"); |
| } |
| |
| private Set<String> getCurrentAppsWithLauncher() { |
| Intent launcherIntent = new Intent(Intent.ACTION_MAIN); |
| launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER); |
| List<ResolveInfo> resolveInfos = mPm.queryIntentActivitiesAsUser(launcherIntent, |
| PackageManager.MATCH_UNINSTALLED_PACKAGES |
| | PackageManager.MATCH_DISABLED_COMPONENTS |
| | PackageManager.MATCH_DIRECT_BOOT_AWARE |
| | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, |
| mUserId); |
| Set<String> apps = new HashSet<String>(); |
| for (ResolveInfo resolveInfo : resolveInfos) { |
| apps.add(resolveInfo.activityInfo.packageName); |
| } |
| return apps; |
| } |
| |
| private Set<String> getSystemInputMethods() { |
| // InputMethodManager is final so it cannot be mocked. |
| // So, we're using IInputMethodManager directly because it can be mocked. |
| List<InputMethodInfo> inputMethods = null; |
| try { |
| inputMethods = mIInputMethodManager.getInputMethodList(); |
| } catch (RemoteException e) { |
| ProvisionLogger.loge("Could not communicate with IInputMethodManager", e); |
| return Collections.<String>emptySet(); |
| } |
| Set<String> systemInputMethods = new HashSet<String>(); |
| for (InputMethodInfo inputMethodInfo : inputMethods) { |
| ApplicationInfo applicationInfo = inputMethodInfo.getServiceInfo().applicationInfo; |
| if ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { |
| systemInputMethods.add(inputMethodInfo.getPackageName()); |
| } |
| } |
| return systemInputMethods; |
| } |
| |
| private void writeSystemApps(Set<String> packageNames, File systemAppsFile) { |
| try { |
| FileOutputStream stream = new FileOutputStream(systemAppsFile, false); |
| XmlSerializer serializer = new FastXmlSerializer(); |
| serializer.setOutput(stream, "utf-8"); |
| serializer.startDocument(null, true); |
| serializer.startTag(null, TAG_SYSTEM_APPS); |
| for (String packageName : packageNames) { |
| serializer.startTag(null, TAG_PACKAGE_LIST_ITEM); |
| serializer.attribute(null, ATTR_VALUE, packageName); |
| serializer.endTag(null, TAG_PACKAGE_LIST_ITEM); |
| } |
| serializer.endTag(null, TAG_SYSTEM_APPS); |
| serializer.endDocument(); |
| stream.close(); |
| } catch (IOException e) { |
| ProvisionLogger.loge("IOException trying to write the system apps", e); |
| } |
| } |
| |
| private Set<String> readSystemApps(File systemAppsFile) { |
| Set<String> result = new HashSet<String>(); |
| if (!systemAppsFile.exists()) { |
| return result; |
| } |
| try { |
| FileInputStream stream = new FileInputStream(systemAppsFile); |
| |
| XmlPullParser parser = Xml.newPullParser(); |
| parser.setInput(stream, null); |
| |
| int type = parser.next(); |
| int outerDepth = parser.getDepth(); |
| while ((type=parser.next()) != XmlPullParser.END_DOCUMENT |
| && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { |
| if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { |
| continue; |
| } |
| String tag = parser.getName(); |
| if (tag.equals(TAG_PACKAGE_LIST_ITEM)) { |
| result.add(parser.getAttributeValue(null, ATTR_VALUE)); |
| } else { |
| ProvisionLogger.loge("Unknown tag: " + tag); |
| } |
| } |
| stream.close(); |
| } catch (IOException e) { |
| ProvisionLogger.loge("IOException trying to read the system apps", e); |
| } catch (XmlPullParserException e) { |
| ProvisionLogger.loge("XmlPullParserException trying to read the system apps", e); |
| } |
| return result; |
| } |
| |
| protected Set<String> getRequiredApps() { |
| HashSet<String> requiredApps = new HashSet<String>(); |
| requiredApps.addAll(mRequiredAppsList); |
| requiredApps.addAll(mVendorRequiredAppsList); |
| requiredApps.add(mMdmPackageName); |
| return requiredApps; |
| } |
| |
| private Set<String> getDisallowedApps() { |
| HashSet<String> disallowedApps = new HashSet<String>(); |
| disallowedApps.addAll(mDisallowedAppsList); |
| disallowedApps.addAll(mVendorDisallowedAppsList); |
| return disallowedApps; |
| } |
| |
| /** |
| * Runs the next task when all packages have been deleted or shuts down the activity if package |
| * deletion fails. |
| */ |
| class PackageDeleteObserver extends IPackageDeleteObserver.Stub { |
| private final AtomicInteger mPackageCount = new AtomicInteger(0); |
| |
| public PackageDeleteObserver(int packageCount) { |
| this.mPackageCount.set(packageCount); |
| } |
| |
| @Override |
| public void packageDeleted(String packageName, int returnCode) { |
| if (returnCode != PackageManager.DELETE_SUCCEEDED) { |
| ProvisionLogger.logw( |
| "Could not finish the provisioning: package deletion failed"); |
| mCallback.onError(); |
| return; |
| } |
| int currentPackageCount = mPackageCount.decrementAndGet(); |
| if (currentPackageCount == 0) { |
| ProvisionLogger.logi("All non-required system apps with launcher icon, " |
| + "and all disallowed apps have been uninstalled."); |
| mCallback.onSuccess(); |
| } |
| } |
| } |
| |
| private static IInputMethodManager getIInputMethodManager() { |
| IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE); |
| return IInputMethodManager.Stub.asInterface(b); |
| } |
| |
| public abstract static class Callback { |
| public abstract void onSuccess(); |
| public abstract void onError(); |
| } |
| } |