1/*
2 * Copyright 2014, The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.managedprovisioning.task;
18
19import android.content.BroadcastReceiver;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.pm.ApplicationInfo;
24import android.content.pm.ComponentInfo;
25import android.content.pm.IPackageDeleteObserver;
26import android.content.pm.IPackageManager;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.util.Xml;
33
34import com.android.internal.util.FastXmlSerializer;
35import com.android.managedprovisioning.ProvisionLogger;
36import com.android.managedprovisioning.R;
37
38import java.io.File;
39import java.io.FileInputStream;
40import java.io.FileOutputStream;
41import java.io.IOException;
42
43import java.util.ArrayList;
44import java.util.Arrays;
45import java.util.HashSet;
46import java.util.List;
47import java.util.Set;
48import java.util.concurrent.atomic.AtomicInteger;
49
50import org.xmlpull.v1.XmlPullParser;
51import org.xmlpull.v1.XmlPullParserException;
52import org.xmlpull.v1.XmlSerializer;
53
54/**
55 * Removes all system apps with a launcher that are not required.
56 * Also disables sharing via Bluetooth and Nfc, and components that listen to
57 * ACTION_INSTALL_SHORTCUT.
58 * This class is called a first time when a user is created, but also after a system update.
59 * In this case, it checks if the system apps that have been added need to be disabled.
60 */
61public class DeleteNonRequiredAppsTask {
62    private final Callback mCallback;
63    private final Context mContext;
64    private final IPackageManager mIpm;
65    private final String mMdmPackageName;
66    private final PackageManager mPm;
67    private final int mReqAppsList;
68    private final int mVendorReqAppsList;
69    private final int mUserId;
70    private final boolean mNewProfile; // If we are provisioning a new profile.
71    private final boolean mDisableInstallShortcutListeners;
72
73    private static final String TAG_SYSTEM_APPS = "system-apps";
74    private static final String TAG_PACKAGE_LIST_ITEM = "item";
75    private static final String ATTR_VALUE = "value";
76
77    public DeleteNonRequiredAppsTask(Context context, String mdmPackageName, int userId,
78            int requiredAppsList, int vendorRequiredAppsList, boolean newProfile,
79            boolean disableInstallShortcutListeners, Callback callback) {
80        mCallback = callback;
81        mContext = context;
82        mMdmPackageName = mdmPackageName;
83        mUserId = userId;
84        mIpm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
85        mPm = context.getPackageManager();
86        mReqAppsList = requiredAppsList;
87        mVendorReqAppsList = vendorRequiredAppsList;
88        mNewProfile = newProfile;
89        mDisableInstallShortcutListeners = disableInstallShortcutListeners;
90    }
91
92    public void run() {
93        if (mNewProfile) {
94            disableNfcBluetoothSharing();
95        }
96        deleteNonRequiredApps();
97    }
98
99    private void disableNfcBluetoothSharing() {
100        ProvisionLogger.logd("Disabling Nfc and Bluetooth sharing.");
101        disableComponent(new ComponentName("com.android.nfc", "com.android.nfc.BeamShareActivity"));
102        disableComponent(new ComponentName("com.android.bluetooth",
103                "com.android.bluetooth.opp.BluetoothOppLauncherActivity"));
104    }
105
106    private void deleteNonRequiredApps() {
107        ProvisionLogger.logd("Deleting non required apps.");
108
109        File file = new File(mContext.getFilesDir() + File.separator + "system_apps"
110                + File.separator + "user" + mUserId + ".xml");
111        file.getParentFile().mkdirs(); // Creating the folder if it does not exist
112
113        Set<String> currentApps = getCurrentSystemApps();
114        Set<String> previousApps;
115        if (mNewProfile) {
116            // If this userId was a managed profile before, file may exist. In this case, we ignore
117            // what is in this file.
118            previousApps = new HashSet<String>();
119        } else {
120            if (file.exists()) {
121                previousApps = readSystemApps(file);
122            } else {
123                // If for some reason, the system apps have not been written to file before, we will
124                // not delete any system apps this time.
125                writeSystemApps(currentApps, file);
126                mCallback.onSuccess();
127                return;
128            }
129        }
130        writeSystemApps(currentApps, file);
131        Set<String> newApps = currentApps;
132        newApps.removeAll(previousApps);
133
134        if (mDisableInstallShortcutListeners) {
135            Intent actionShortcut = new Intent("com.android.launcher.action.INSTALL_SHORTCUT");
136            if (previousApps.isEmpty()) {
137                // Here, all the apps are in newApps.
138                // It is faster to do it this way than to go through all the apps one by one.
139                disableReceivers(actionShortcut);
140            } else {
141                // Here, all the apps are not in newApps. So we have to go through all the new
142                // apps one by one.
143                for (String newApp : newApps) {
144                    actionShortcut.setPackage(newApp);
145                    disableReceivers(actionShortcut);
146                }
147            }
148        }
149        Set<String> packagesToDelete = newApps;
150        packagesToDelete.removeAll(getRequiredApps());
151        packagesToDelete.retainAll(getCurrentAppsWithLauncher());
152        // com.android.server.telecom should not handle CALL intents in the managed profile.
153        if (mNewProfile) {
154            packagesToDelete.add("com.android.server.telecom");
155        }
156        int size = packagesToDelete.size();
157        if (size > 0) {
158            PackageDeleteObserver packageDeleteObserver =
159                        new PackageDeleteObserver(packagesToDelete.size());
160            for (String packageName : packagesToDelete) {
161                try {
162                    mIpm.deletePackageAsUser(packageName, packageDeleteObserver, mUserId,
163                            PackageManager.DELETE_SYSTEM_APP);
164                } catch (RemoteException neverThrown) {
165                    // Never thrown, as we are making local calls.
166                    ProvisionLogger.loge("This should not happen.", neverThrown);
167                }
168            }
169        } else {
170            mCallback.onSuccess();
171        }
172    }
173
174    private void disableReceivers(Intent intent) {
175        List<ResolveInfo> receivers = mPm.queryBroadcastReceivers(intent, 0, mUserId);
176        for (ResolveInfo ri : receivers) {
177            // One of ri.activityInfo, ri.serviceInfo, ri.providerInfo is not null. Let's find which
178            // one.
179            ComponentInfo ci;
180            if (ri.activityInfo != null) {
181                ci = ri.activityInfo;
182            } else if (ri.serviceInfo != null) {
183                ci = ri.serviceInfo;
184            } else {
185                ci = ri.providerInfo;
186            }
187            disableComponent(new ComponentName(ci.packageName, ci.name));
188        }
189    }
190
191    private void disableComponent(ComponentName toDisable) {
192        try {
193            mIpm.setComponentEnabledSetting(toDisable,
194                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP,
195                    mUserId);
196        } catch (RemoteException neverThrown) {
197            ProvisionLogger.loge("This should not happen.", neverThrown);
198        } catch (Exception e) {
199            ProvisionLogger.logw("Component not found, not disabling it: "
200                + toDisable.toShortString());
201        }
202    }
203
204    /**
205     * Returns the set of package names of apps that are in the system image,
206     * whether they have been deleted or not.
207     */
208    private Set<String> getCurrentSystemApps() {
209        Set<String> apps = new HashSet<String>();
210        List<ApplicationInfo> aInfos = null;
211        try {
212            aInfos = mIpm.getInstalledApplications(
213                    PackageManager.GET_UNINSTALLED_PACKAGES, mUserId).getList();
214        } catch (RemoteException neverThrown) {
215            // Never thrown, as we are making local calls.
216            ProvisionLogger.loge("This should not happen.", neverThrown);
217        }
218        for (ApplicationInfo aInfo : aInfos) {
219            if ((aInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
220                apps.add(aInfo.packageName);
221            }
222        }
223        return apps;
224    }
225
226    private Set<String> getCurrentAppsWithLauncher() {
227        Intent launcherIntent = new Intent(Intent.ACTION_MAIN);
228        launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
229        List<ResolveInfo> resolveInfos = mPm.queryIntentActivitiesAsUser(launcherIntent,
230                PackageManager.GET_UNINSTALLED_PACKAGES, mUserId);
231        Set<String> apps = new HashSet<String>();
232        for (ResolveInfo resolveInfo : resolveInfos) {
233            apps.add(resolveInfo.activityInfo.packageName);
234        }
235        return apps;
236    }
237
238    private void writeSystemApps(Set<String> packageNames, File file) {
239        try {
240            FileOutputStream stream = new FileOutputStream(file, false);
241            XmlSerializer serializer = new FastXmlSerializer();
242            serializer.setOutput(stream, "utf-8");
243            serializer.startDocument(null, true);
244            serializer.startTag(null, TAG_SYSTEM_APPS);
245            for (String packageName : packageNames) {
246                serializer.startTag(null, TAG_PACKAGE_LIST_ITEM);
247                serializer.attribute(null, ATTR_VALUE, packageName);
248                serializer.endTag(null, TAG_PACKAGE_LIST_ITEM);
249            }
250            serializer.endTag(null, TAG_SYSTEM_APPS);
251            serializer.endDocument();
252            stream.close();
253        } catch (IOException e) {
254            ProvisionLogger.loge("IOException trying to write the system apps", e);
255        }
256    }
257
258    private Set<String> readSystemApps(File file) {
259        Set<String> result = new HashSet<String>();
260        if (!file.exists()) {
261            return result;
262        }
263        try {
264            FileInputStream stream = new FileInputStream(file);
265
266            XmlPullParser parser = Xml.newPullParser();
267            parser.setInput(stream, null);
268
269            int type = parser.next();
270            int outerDepth = parser.getDepth();
271            while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
272                   && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
273                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
274                    continue;
275                }
276                String tag = parser.getName();
277                if (tag.equals(TAG_PACKAGE_LIST_ITEM)) {
278                    result.add(parser.getAttributeValue(null, ATTR_VALUE));
279                } else {
280                    ProvisionLogger.loge("Unknown tag: " + tag);
281                }
282            }
283            stream.close();
284        } catch (IOException e) {
285            ProvisionLogger.loge("IOException trying to read the system apps", e);
286        } catch (XmlPullParserException e) {
287            ProvisionLogger.loge("XmlPullParserException trying to read the system apps", e);
288        }
289        return result;
290    }
291
292    protected Set<String> getRequiredApps() {
293        HashSet<String> requiredApps = new HashSet<String> (Arrays.asList(
294                        mContext.getResources().getStringArray(mReqAppsList)));
295        requiredApps.addAll(Arrays.asList(
296                        mContext.getResources().getStringArray(mVendorReqAppsList)));
297        requiredApps.add(mMdmPackageName);
298        return requiredApps;
299    }
300
301    /**
302     * Runs the next task when all packages have been deleted or shuts down the activity if package
303     * deletion fails.
304     */
305    class PackageDeleteObserver extends IPackageDeleteObserver.Stub {
306        private final AtomicInteger mPackageCount = new AtomicInteger(0);
307
308        public PackageDeleteObserver(int packageCount) {
309            this.mPackageCount.set(packageCount);
310        }
311
312        @Override
313        public void packageDeleted(String packageName, int returnCode) {
314            if (returnCode != PackageManager.DELETE_SUCCEEDED) {
315                ProvisionLogger.logw(
316                        "Could not finish the provisioning: package deletion failed");
317                mCallback.onError();
318            }
319            int currentPackageCount = mPackageCount.decrementAndGet();
320            if (currentPackageCount == 0) {
321                ProvisionLogger.logi("All non-required system apps have been uninstalled.");
322                mCallback.onSuccess();
323            }
324        }
325    }
326
327    public abstract static class Callback {
328        public abstract void onSuccess();
329        public abstract void onError();
330    }
331}
332