1/*
2 * Copyright (C) 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.server.am;
18
19import static com.android.server.am.ActivityManagerDebugConfig.*;
20import static com.android.server.am.ActivityStackSupervisor.PRESERVE_WINDOWS;
21
22import java.io.File;
23import java.io.FileInputStream;
24import java.io.FileOutputStream;
25import java.nio.charset.StandardCharsets;
26import java.util.HashMap;
27import java.util.Iterator;
28import java.util.Map;
29
30import org.xmlpull.v1.XmlPullParser;
31import org.xmlpull.v1.XmlPullParserException;
32import org.xmlpull.v1.XmlSerializer;
33
34import com.android.internal.util.FastXmlSerializer;
35
36import android.app.ActivityManager;
37import android.app.AppGlobals;
38import android.content.pm.ApplicationInfo;
39import android.content.pm.IPackageManager;
40import android.content.res.CompatibilityInfo;
41import android.content.res.Configuration;
42import android.os.Handler;
43import android.os.Looper;
44import android.os.Message;
45import android.os.RemoteException;
46import android.util.AtomicFile;
47import android.util.Slog;
48import android.util.Xml;
49
50public final class CompatModePackages {
51    private static final String TAG = TAG_WITH_CLASS_NAME ? "CompatModePackages" : TAG_AM;
52    private static final String TAG_CONFIGURATION = TAG + POSTFIX_CONFIGURATION;
53
54    private final ActivityManagerService mService;
55    private final AtomicFile mFile;
56
57    // Compatibility state: no longer ask user to select the mode.
58    public static final int COMPAT_FLAG_DONT_ASK = 1<<0;
59    // Compatibility state: compatibility mode is enabled.
60    public static final int COMPAT_FLAG_ENABLED = 1<<1;
61    // Unsupported zoom state: don't warn the user about unsupported zoom mode.
62    public static final int UNSUPPORTED_ZOOM_FLAG_DONT_NOTIFY = 1<<2;
63
64    private final HashMap<String, Integer> mPackages = new HashMap<String, Integer>();
65
66    private static final int MSG_WRITE = ActivityManagerService.FIRST_COMPAT_MODE_MSG;
67
68    private final CompatHandler mHandler;
69
70    private final class CompatHandler extends Handler {
71        public CompatHandler(Looper looper) {
72            super(looper, null, true);
73        }
74
75        @Override
76        public void handleMessage(Message msg) {
77            switch (msg.what) {
78                case MSG_WRITE:
79                    saveCompatModes();
80                    break;
81            }
82        }
83    };
84
85    public CompatModePackages(ActivityManagerService service, File systemDir, Handler handler) {
86        mService = service;
87        mFile = new AtomicFile(new File(systemDir, "packages-compat.xml"));
88        mHandler = new CompatHandler(handler.getLooper());
89
90        FileInputStream fis = null;
91        try {
92            fis = mFile.openRead();
93            XmlPullParser parser = Xml.newPullParser();
94            parser.setInput(fis, StandardCharsets.UTF_8.name());
95            int eventType = parser.getEventType();
96            while (eventType != XmlPullParser.START_TAG &&
97                    eventType != XmlPullParser.END_DOCUMENT) {
98                eventType = parser.next();
99            }
100            if (eventType == XmlPullParser.END_DOCUMENT) {
101                return;
102            }
103
104            String tagName = parser.getName();
105            if ("compat-packages".equals(tagName)) {
106                eventType = parser.next();
107                do {
108                    if (eventType == XmlPullParser.START_TAG) {
109                        tagName = parser.getName();
110                        if (parser.getDepth() == 2) {
111                            if ("pkg".equals(tagName)) {
112                                String pkg = parser.getAttributeValue(null, "name");
113                                if (pkg != null) {
114                                    String mode = parser.getAttributeValue(null, "mode");
115                                    int modeInt = 0;
116                                    if (mode != null) {
117                                        try {
118                                            modeInt = Integer.parseInt(mode);
119                                        } catch (NumberFormatException e) {
120                                        }
121                                    }
122                                    mPackages.put(pkg, modeInt);
123                                }
124                            }
125                        }
126                    }
127                    eventType = parser.next();
128                } while (eventType != XmlPullParser.END_DOCUMENT);
129            }
130        } catch (XmlPullParserException e) {
131            Slog.w(TAG, "Error reading compat-packages", e);
132        } catch (java.io.IOException e) {
133            if (fis != null) Slog.w(TAG, "Error reading compat-packages", e);
134        } finally {
135            if (fis != null) {
136                try {
137                    fis.close();
138                } catch (java.io.IOException e1) {
139                }
140            }
141        }
142    }
143
144    public HashMap<String, Integer> getPackages() {
145        return mPackages;
146    }
147
148    private int getPackageFlags(String packageName) {
149        Integer flags = mPackages.get(packageName);
150        return flags != null ? flags : 0;
151    }
152
153    public void handlePackageDataClearedLocked(String packageName) {
154        // User has explicitly asked to clear all associated data.
155        removePackage(packageName);
156    }
157
158    public void handlePackageUninstalledLocked(String packageName) {
159        // Clear settings when app is uninstalled since this is an explicit
160        // signal from the user to remove the app and all associated data.
161        removePackage(packageName);
162    }
163
164    private void removePackage(String packageName) {
165        if (mPackages.containsKey(packageName)) {
166            mPackages.remove(packageName);
167            scheduleWrite();
168        }
169    }
170
171    public void handlePackageAddedLocked(String packageName, boolean updated) {
172        ApplicationInfo ai = null;
173        try {
174            ai = AppGlobals.getPackageManager().getApplicationInfo(packageName, 0, 0);
175        } catch (RemoteException e) {
176        }
177        if (ai == null) {
178            return;
179        }
180        CompatibilityInfo ci = compatibilityInfoForPackageLocked(ai);
181        final boolean mayCompat = !ci.alwaysSupportsScreen()
182                && !ci.neverSupportsScreen();
183
184        if (updated) {
185            // Update -- if the app no longer can run in compat mode, clear
186            // any current settings for it.
187            if (!mayCompat && mPackages.containsKey(packageName)) {
188                mPackages.remove(packageName);
189                scheduleWrite();
190            }
191        }
192    }
193
194    private void scheduleWrite() {
195        mHandler.removeMessages(MSG_WRITE);
196        Message msg = mHandler.obtainMessage(MSG_WRITE);
197        mHandler.sendMessageDelayed(msg, 10000);
198    }
199
200    public CompatibilityInfo compatibilityInfoForPackageLocked(ApplicationInfo ai) {
201        final Configuration globalConfig = mService.getGlobalConfiguration();
202        CompatibilityInfo ci = new CompatibilityInfo(ai, globalConfig.screenLayout,
203                globalConfig.smallestScreenWidthDp,
204                (getPackageFlags(ai.packageName)&COMPAT_FLAG_ENABLED) != 0);
205        //Slog.i(TAG, "*********** COMPAT FOR PKG " + ai.packageName + ": " + ci);
206        return ci;
207    }
208
209    public int computeCompatModeLocked(ApplicationInfo ai) {
210        final boolean enabled = (getPackageFlags(ai.packageName)&COMPAT_FLAG_ENABLED) != 0;
211        final Configuration globalConfig = mService.getGlobalConfiguration();
212        final CompatibilityInfo info = new CompatibilityInfo(ai, globalConfig.screenLayout,
213                globalConfig.smallestScreenWidthDp, enabled);
214        if (info.alwaysSupportsScreen()) {
215            return ActivityManager.COMPAT_MODE_NEVER;
216        }
217        if (info.neverSupportsScreen()) {
218            return ActivityManager.COMPAT_MODE_ALWAYS;
219        }
220        return enabled ? ActivityManager.COMPAT_MODE_ENABLED
221                : ActivityManager.COMPAT_MODE_DISABLED;
222    }
223
224    public boolean getFrontActivityAskCompatModeLocked() {
225        ActivityRecord r = mService.getFocusedStack().topRunningActivityLocked();
226        if (r == null) {
227            return false;
228        }
229        return getPackageAskCompatModeLocked(r.packageName);
230    }
231
232    public boolean getPackageAskCompatModeLocked(String packageName) {
233        return (getPackageFlags(packageName)&COMPAT_FLAG_DONT_ASK) == 0;
234    }
235
236    public boolean getPackageNotifyUnsupportedZoomLocked(String packageName) {
237        return (getPackageFlags(packageName)&UNSUPPORTED_ZOOM_FLAG_DONT_NOTIFY) == 0;
238    }
239
240    public void setFrontActivityAskCompatModeLocked(boolean ask) {
241        ActivityRecord r = mService.getFocusedStack().topRunningActivityLocked();
242        if (r != null) {
243            setPackageAskCompatModeLocked(r.packageName, ask);
244        }
245    }
246
247    public void setPackageAskCompatModeLocked(String packageName, boolean ask) {
248        int curFlags = getPackageFlags(packageName);
249        int newFlags = ask ? (curFlags&~COMPAT_FLAG_DONT_ASK) : (curFlags|COMPAT_FLAG_DONT_ASK);
250        if (curFlags != newFlags) {
251            if (newFlags != 0) {
252                mPackages.put(packageName, newFlags);
253            } else {
254                mPackages.remove(packageName);
255            }
256            scheduleWrite();
257        }
258    }
259
260    public void setPackageNotifyUnsupportedZoomLocked(String packageName, boolean notify) {
261        final int curFlags = getPackageFlags(packageName);
262        final int newFlags = notify ? (curFlags&~UNSUPPORTED_ZOOM_FLAG_DONT_NOTIFY) :
263                (curFlags|UNSUPPORTED_ZOOM_FLAG_DONT_NOTIFY);
264        if (curFlags != newFlags) {
265            if (newFlags != 0) {
266                mPackages.put(packageName, newFlags);
267            } else {
268                mPackages.remove(packageName);
269            }
270            scheduleWrite();
271        }
272    }
273
274    public int getFrontActivityScreenCompatModeLocked() {
275        ActivityRecord r = mService.getFocusedStack().topRunningActivityLocked();
276        if (r == null) {
277            return ActivityManager.COMPAT_MODE_UNKNOWN;
278        }
279        return computeCompatModeLocked(r.info.applicationInfo);
280    }
281
282    public void setFrontActivityScreenCompatModeLocked(int mode) {
283        ActivityRecord r = mService.getFocusedStack().topRunningActivityLocked();
284        if (r == null) {
285            Slog.w(TAG, "setFrontActivityScreenCompatMode failed: no top activity");
286            return;
287        }
288        setPackageScreenCompatModeLocked(r.info.applicationInfo, mode);
289    }
290
291    public int getPackageScreenCompatModeLocked(String packageName) {
292        ApplicationInfo ai = null;
293        try {
294            ai = AppGlobals.getPackageManager().getApplicationInfo(packageName, 0, 0);
295        } catch (RemoteException e) {
296        }
297        if (ai == null) {
298            return ActivityManager.COMPAT_MODE_UNKNOWN;
299        }
300        return computeCompatModeLocked(ai);
301    }
302
303    public void setPackageScreenCompatModeLocked(String packageName, int mode) {
304        ApplicationInfo ai = null;
305        try {
306            ai = AppGlobals.getPackageManager().getApplicationInfo(packageName, 0, 0);
307        } catch (RemoteException e) {
308        }
309        if (ai == null) {
310            Slog.w(TAG, "setPackageScreenCompatMode failed: unknown package " + packageName);
311            return;
312        }
313        setPackageScreenCompatModeLocked(ai, mode);
314    }
315
316    private void setPackageScreenCompatModeLocked(ApplicationInfo ai, int mode) {
317        final String packageName = ai.packageName;
318
319        int curFlags = getPackageFlags(packageName);
320
321        boolean enable;
322        switch (mode) {
323            case ActivityManager.COMPAT_MODE_DISABLED:
324                enable = false;
325                break;
326            case ActivityManager.COMPAT_MODE_ENABLED:
327                enable = true;
328                break;
329            case ActivityManager.COMPAT_MODE_TOGGLE:
330                enable = (curFlags&COMPAT_FLAG_ENABLED) == 0;
331                break;
332            default:
333                Slog.w(TAG, "Unknown screen compat mode req #" + mode + "; ignoring");
334                return;
335        }
336
337        int newFlags = curFlags;
338        if (enable) {
339            newFlags |= COMPAT_FLAG_ENABLED;
340        } else {
341            newFlags &= ~COMPAT_FLAG_ENABLED;
342        }
343
344        CompatibilityInfo ci = compatibilityInfoForPackageLocked(ai);
345        if (ci.alwaysSupportsScreen()) {
346            Slog.w(TAG, "Ignoring compat mode change of " + packageName
347                    + "; compatibility never needed");
348            newFlags = 0;
349        }
350        if (ci.neverSupportsScreen()) {
351            Slog.w(TAG, "Ignoring compat mode change of " + packageName
352                    + "; compatibility always needed");
353            newFlags = 0;
354        }
355
356        if (newFlags != curFlags) {
357            if (newFlags != 0) {
358                mPackages.put(packageName, newFlags);
359            } else {
360                mPackages.remove(packageName);
361            }
362
363            // Need to get compatibility info in new state.
364            ci = compatibilityInfoForPackageLocked(ai);
365
366            scheduleWrite();
367
368            final ActivityStack stack = mService.getFocusedStack();
369            ActivityRecord starting = stack.restartPackage(packageName);
370
371            // Tell all processes that loaded this package about the change.
372            for (int i=mService.mLruProcesses.size()-1; i>=0; i--) {
373                ProcessRecord app = mService.mLruProcesses.get(i);
374                if (!app.pkgList.containsKey(packageName)) {
375                    continue;
376                }
377                try {
378                    if (app.thread != null) {
379                        if (DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION, "Sending to proc "
380                                + app.processName + " new compat " + ci);
381                        app.thread.updatePackageCompatibilityInfo(packageName, ci);
382                    }
383                } catch (Exception e) {
384                }
385            }
386
387            if (starting != null) {
388                starting.ensureActivityConfigurationLocked(0 /* globalChanges */,
389                        false /* preserveWindow */);
390                // And we need to make sure at this point that all other activities
391                // are made visible with the correct configuration.
392                stack.ensureActivitiesVisibleLocked(starting, 0, !PRESERVE_WINDOWS);
393            }
394        }
395    }
396
397    void saveCompatModes() {
398        HashMap<String, Integer> pkgs;
399        synchronized (mService) {
400            pkgs = new HashMap<String, Integer>(mPackages);
401        }
402
403        FileOutputStream fos = null;
404
405        try {
406            fos = mFile.startWrite();
407            XmlSerializer out = new FastXmlSerializer();
408            out.setOutput(fos, StandardCharsets.UTF_8.name());
409            out.startDocument(null, true);
410            out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
411            out.startTag(null, "compat-packages");
412
413            final IPackageManager pm = AppGlobals.getPackageManager();
414            final Configuration globalConfig = mService.getGlobalConfiguration();
415            final int screenLayout = globalConfig.screenLayout;
416            final int smallestScreenWidthDp = globalConfig.smallestScreenWidthDp;
417            final Iterator<Map.Entry<String, Integer>> it = pkgs.entrySet().iterator();
418            while (it.hasNext()) {
419                Map.Entry<String, Integer> entry = it.next();
420                String pkg = entry.getKey();
421                int mode = entry.getValue();
422                if (mode == 0) {
423                    continue;
424                }
425                ApplicationInfo ai = null;
426                try {
427                    ai = pm.getApplicationInfo(pkg, 0, 0);
428                } catch (RemoteException e) {
429                }
430                if (ai == null) {
431                    continue;
432                }
433                CompatibilityInfo info = new CompatibilityInfo(ai, screenLayout,
434                        smallestScreenWidthDp, false);
435                if (info.alwaysSupportsScreen()) {
436                    continue;
437                }
438                if (info.neverSupportsScreen()) {
439                    continue;
440                }
441                out.startTag(null, "pkg");
442                out.attribute(null, "name", pkg);
443                out.attribute(null, "mode", Integer.toString(mode));
444                out.endTag(null, "pkg");
445            }
446
447            out.endTag(null, "compat-packages");
448            out.endDocument();
449
450            mFile.finishWrite(fos);
451        } catch (java.io.IOException e1) {
452            Slog.w(TAG, "Error writing compat packages", e1);
453            if (fos != null) {
454                mFile.failWrite(fos);
455            }
456        }
457    }
458}
459