content: /*
   * Copyright (C) 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.server.wm;
  
  import static com.android.server.wm.ActivityStackSupervisor.PRESERVE_WINDOWS;
  import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_CONFIGURATION;
  import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_CONFIGURATION;
  import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
  import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.nio.charset.StandardCharsets;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.Map;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  import org.xmlpull.v1.XmlSerializer;
  
  import com.android.internal.util.FastXmlSerializer;
  
  import android.app.ActivityManager;
  import android.app.AppGlobals;
  import android.content.pm.ApplicationInfo;
  import android.content.pm.IPackageManager;
  import android.content.res.CompatibilityInfo;
  import android.content.res.Configuration;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.os.RemoteException;
  import android.util.AtomicFile;
  import android.util.Slog;
  import android.util.SparseArray;
  import android.util.Xml;
  
  public final class CompatModePackages {
      private static final String TAG = TAG_WITH_CLASS_NAME ? "CompatModePackages" : TAG_ATM;
      private static final String TAG_CONFIGURATION = TAG + POSTFIX_CONFIGURATION;
  
      private final ActivityTaskManagerService mService;
      private final AtomicFile mFile;
  
      // Compatibility state: no longer ask user to select the mode.
      private static final int COMPAT_FLAG_DONT_ASK = 1<<0;
      // Compatibility state: compatibility mode is enabled.
      private static final int COMPAT_FLAG_ENABLED = 1<<1;
  
      private final HashMap<String, Integer> mPackages = new HashMap<String, Integer>();
  
      private static final int MSG_WRITE = 300;
  
      private final CompatHandler mHandler;
  
      private final class CompatHandler extends Handler {
          public CompatHandler(Looper looper) {
              super(looper, null, true);
          }
  
          @Override
          public void handleMessage(Message msg) {
              switch (msg.what) {
                  case MSG_WRITE:
                      saveCompatModes();
                      break;
              }
          }
      };
  
      public CompatModePackages(ActivityTaskManagerService service, File systemDir, Handler handler) {
          mService = service;
          mFile = new AtomicFile(new File(systemDir, "packages-compat.xml"), "compat-mode");
          mHandler = new CompatHandler(handler.getLooper());
  
          FileInputStream fis = null;
          try {
              fis = mFile.openRead();
              XmlPullParser parser = Xml.newPullParser();
              parser.setInput(fis, StandardCharsets.UTF_8.name());
              int eventType = parser.getEventType();
              while (eventType != XmlPullParser.START_TAG &&
                      eventType != XmlPullParser.END_DOCUMENT) {
                  eventType = parser.next();
              }
              if (eventType == XmlPullParser.END_DOCUMENT) {
                  return;
              }
  
              String tagName = parser.getName();
              if ("compat-packages".equals(tagName)) {
                  eventType = parser.next();
                  do {
                      if (eventType == XmlPullParser.START_TAG) {
                          tagName = parser.getName();
                          if (parser.getDepth() == 2) {
                              if ("pkg".equals(tagName)) {
                                  String pkg = parser.getAttributeValue(null, "name");
                                  if (pkg != null) {
                                      String mode = parser.getAttributeValue(null, "mode");
                                      int modeInt = 0;
                                      if (mode != null) {
                                          try {
                                              modeInt = Integer.parseInt(mode);
                                          } catch (NumberFormatException e) {
                                          }
                                      }
                                      mPackages.put(pkg, modeInt);
                                  }
                              }
                          }
                      }
                      eventType = parser.next();
                  } while (eventType != XmlPullParser.END_DOCUMENT);
              }
          } catch (XmlPullParserException e) {
              Slog.w(TAG, "Error reading compat-packages", e);
          } catch (java.io.IOException e) {
              if (fis != null) Slog.w(TAG, "Error reading compat-packages", e);
          } finally {
              if (fis != null) {
                  try {
                      fis.close();
                  } catch (java.io.IOException e1) {
                  }
              }
          }
      }
  
      public HashMap<String, Integer> getPackages() {
          return mPackages;
      }
  
      private int getPackageFlags(String packageName) {
          Integer flags = mPackages.get(packageName);
          return flags != null ? flags : 0;
      }
  
      public void handlePackageDataClearedLocked(String packageName) {
          // User has explicitly asked to clear all associated data.
          removePackage(packageName);
      }
  
      public void handlePackageUninstalledLocked(String packageName) {
          // Clear settings when app is uninstalled since this is an explicit
          // signal from the user to remove the app and all associated data.
          removePackage(packageName);
      }
  
      private void removePackage(String packageName) {
          if (mPackages.containsKey(packageName)) {
              mPackages.remove(packageName);
              scheduleWrite();
          }
      }
  
      public void handlePackageAddedLocked(String packageName, boolean updated) {
          ApplicationInfo ai = null;
          try {
              ai = AppGlobals.getPackageManager().getApplicationInfo(packageName, 0, 0);
          } catch (RemoteException e) {
          }
          if (ai == null) {
              return;
          }
          CompatibilityInfo ci = compatibilityInfoForPackageLocked(ai);
          final boolean mayCompat = !ci.alwaysSupportsScreen()
                  && !ci.neverSupportsScreen();
  
          if (updated) {
              // Update -- if the app no longer can run in compat mode, clear
              // any current settings for it.
              if (!mayCompat && mPackages.containsKey(packageName)) {
                  mPackages.remove(packageName);
                  scheduleWrite();
              }
          }
      }
  
      private void scheduleWrite() {
          mHandler.removeMessages(MSG_WRITE);
          Message msg = mHandler.obtainMessage(MSG_WRITE);
          mHandler.sendMessageDelayed(msg, 10000);
      }
  
      public CompatibilityInfo compatibilityInfoForPackageLocked(ApplicationInfo ai) {
          final Configuration globalConfig = mService.getGlobalConfiguration();
          CompatibilityInfo ci = new CompatibilityInfo(ai, globalConfig.screenLayout,
                  globalConfig.smallestScreenWidthDp,
                  (getPackageFlags(ai.packageName)&COMPAT_FLAG_ENABLED) != 0);
          //Slog.i(TAG, "*********** COMPAT FOR PKG " + ai.packageName + ": " + ci);
          return ci;
      }
  
      public int computeCompatModeLocked(ApplicationInfo ai) {
          final boolean enabled = (getPackageFlags(ai.packageName)&COMPAT_FLAG_ENABLED) != 0;
          final Configuration globalConfig = mService.getGlobalConfiguration();
          final CompatibilityInfo info = new CompatibilityInfo(ai, globalConfig.screenLayout,
                  globalConfig.smallestScreenWidthDp, enabled);
          if (info.alwaysSupportsScreen()) {
              return ActivityManager.COMPAT_MODE_NEVER;
          }
          if (info.neverSupportsScreen()) {
              return ActivityManager.COMPAT_MODE_ALWAYS;
          }
          return enabled ? ActivityManager.COMPAT_MODE_ENABLED
                  : ActivityManager.COMPAT_MODE_DISABLED;
      }
  
      public boolean getPackageAskCompatModeLocked(String packageName) {
          return (getPackageFlags(packageName)&COMPAT_FLAG_DONT_ASK) == 0;
      }
  
      public void setPackageAskCompatModeLocked(String packageName, boolean ask) {
          setPackageFlagLocked(packageName, COMPAT_FLAG_DONT_ASK, ask);
      }
  
      private void setPackageFlagLocked(String packageName, int flag, boolean set) {
          final int curFlags = getPackageFlags(packageName);
          final int newFlags = set ? (curFlags & ~flag) : (curFlags | flag);
          if (curFlags != newFlags) {
              if (newFlags != 0) {
                  mPackages.put(packageName, newFlags);
              } else {
                  mPackages.remove(packageName);
              }
              scheduleWrite();
          }
      }
  
      public int getPackageScreenCompatModeLocked(String packageName) {
          ApplicationInfo ai = null;
          try {
              ai = AppGlobals.getPackageManager().getApplicationInfo(packageName, 0, 0);
          } catch (RemoteException e) {
          }
          if (ai == null) {
              return ActivityManager.COMPAT_MODE_UNKNOWN;
          }
          return computeCompatModeLocked(ai);
      }
  
      public void setPackageScreenCompatModeLocked(String packageName, int mode) {
          ApplicationInfo ai = null;
          try {
              ai = AppGlobals.getPackageManager().getApplicationInfo(packageName, 0, 0);
          } catch (RemoteException e) {
          }
          if (ai == null) {
              Slog.w(TAG, "setPackageScreenCompatMode failed: unknown package " + packageName);
              return;
          }
          setPackageScreenCompatModeLocked(ai, mode);
      }
  
      void setPackageScreenCompatModeLocked(ApplicationInfo ai, int mode) {
          final String packageName = ai.packageName;
  
          int curFlags = getPackageFlags(packageName);
  
          boolean enable;
          switch (mode) {
              case ActivityManager.COMPAT_MODE_DISABLED:
                  enable = false;
                  break;
              case ActivityManager.COMPAT_MODE_ENABLED:
                  enable = true;
                  break;
              case ActivityManager.COMPAT_MODE_TOGGLE:
                  enable = (curFlags&COMPAT_FLAG_ENABLED) == 0;
                  break;
              default:
                  Slog.w(TAG, "Unknown screen compat mode req #" + mode + "; ignoring");
                  return;
          }
  
          int newFlags = curFlags;
          if (enable) {
              newFlags |= COMPAT_FLAG_ENABLED;
          } else {
              newFlags &= ~COMPAT_FLAG_ENABLED;
          }
  
          CompatibilityInfo ci = compatibilityInfoForPackageLocked(ai);
          if (ci.alwaysSupportsScreen()) {
              Slog.w(TAG, "Ignoring compat mode change of " + packageName
                      + "; compatibility never needed");
              newFlags = 0;
          }
          if (ci.neverSupportsScreen()) {
              Slog.w(TAG, "Ignoring compat mode change of " + packageName
                      + "; compatibility always needed");
              newFlags = 0;
          }
  
          if (newFlags != curFlags) {
              if (newFlags != 0) {
                  mPackages.put(packageName, newFlags);
              } else {
                  mPackages.remove(packageName);
              }
  
              // Need to get compatibility info in new state.
              ci = compatibilityInfoForPackageLocked(ai);
  
              scheduleWrite();
  
              final ActivityStack stack = mService.getTopDisplayFocusedStack();
              ActivityRecord starting = stack.restartPackage(packageName);
  
              // Tell all processes that loaded this package about the change.
              SparseArray<WindowProcessController> pidMap = mService.mProcessMap.getPidMap();
              for (int i = pidMap.size() - 1; i >= 0; i--) {
                  final WindowProcessController app = pidMap.valueAt(i);
                  if (!app.mPkgList.contains(packageName)) {
                      continue;
                  }
                  try {
                      if (app.hasThread()) {
                          if (DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION, "Sending to proc "
                                  + app.mName + " new compat " + ci);
                          app.getThread().updatePackageCompatibilityInfo(packageName, ci);
                      }
                  } catch (Exception e) {
                  }
              }
  
              if (starting != null) {
                  starting.ensureActivityConfiguration(0 /* globalChanges */,
                          false /* preserveWindow */);
                  // And we need to make sure at this point that all other activities
                  // are made visible with the correct configuration.
                  stack.ensureActivitiesVisibleLocked(starting, 0, !PRESERVE_WINDOWS);
              }
          }
      }
  
      private void saveCompatModes() {
          HashMap<String, Integer> pkgs;
          synchronized (mService.mGlobalLock) {
              pkgs = new HashMap<>(mPackages);
          }
  
          FileOutputStream fos = null;
  
          try {
              fos = mFile.startWrite();
              XmlSerializer out = new FastXmlSerializer();
              out.setOutput(fos, StandardCharsets.UTF_8.name());
              out.startDocument(null, true);
              out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
              out.startTag(null, "compat-packages");
  
              final IPackageManager pm = AppGlobals.getPackageManager();
              final Configuration globalConfig = mService.getGlobalConfiguration();
              final int screenLayout = globalConfig.screenLayout;
              final int smallestScreenWidthDp = globalConfig.smallestScreenWidthDp;
              final Iterator<Map.Entry<String, Integer>> it = pkgs.entrySet().iterator();
              while (it.hasNext()) {
                  Map.Entry<String, Integer> entry = it.next();
                  String pkg = entry.getKey();
                  int mode = entry.getValue();
                  if (mode == 0) {
                      continue;
                  }
                  ApplicationInfo ai = null;
                  try {
                      ai = pm.getApplicationInfo(pkg, 0, 0);
                  } catch (RemoteException e) {
                  }
                  if (ai == null) {
                      continue;
                  }
                  CompatibilityInfo info = new CompatibilityInfo(ai, screenLayout,
                          smallestScreenWidthDp, false);
                  if (info.alwaysSupportsScreen()) {
                      continue;
                  }
                  if (info.neverSupportsScreen()) {
                      continue;
                  }
                  out.startTag(null, "pkg");
                  out.attribute(null, "name", pkg);
                  out.attribute(null, "mode", Integer.toString(mode));
                  out.endTag(null, "pkg");
              }
  
              out.endTag(null, "compat-packages");
              out.endDocument();
  
              mFile.finishWrite(fos);
          } catch (java.io.IOException e1) {
              Slog.w(TAG, "Error writing compat packages", e1);
              if (fos != null) {
                  mFile.failWrite(fos);
              }
          }
      }
  }
  