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