Handling configuration changes at runtime instead of killing the process

Change-Id: I0c7f7e58ddb690f371c257b6142a4e918e9acb7f
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index f3f2238..0b2f4d9 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.config.FeatureFlags.APPLY_CONFIG_AT_RUNTIME;
+
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -23,6 +25,7 @@
 import android.content.res.XmlResourceParser;
 import android.graphics.Point;
 import android.util.DisplayMetrics;
+import android.util.Log;
 import android.util.Xml;
 import android.view.Display;
 import android.view.WindowManager;
@@ -45,13 +48,13 @@
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<InvariantDeviceProfile> INSTANCE =
-            new MainThreadInitializedObject<>((c) -> {
-                new ConfigMonitor(c).register();
-                return new InvariantDeviceProfile(c);
-            });
+            new MainThreadInitializedObject<>(InvariantDeviceProfile::new);
 
     private static final float ICON_SIZE_DEFINED_IN_APP_DP = 48;
 
+    public static final int CHANGE_FLAG_GRID = 1 << 0;
+    public static final int CHANGE_FLAG_ICON_SIZE = 1 << 1;
+
     // Constants that affects the interpolation curve between statically defined device profile
     // buckets.
     private static float KNEARESTNEIGHBOR = 3;
@@ -61,9 +64,9 @@
     private static float WEIGHT_EFFICIENT = 100000f;
 
     // Profile-defining invariant properties
-    String name;
-    float minWidthDps;
-    float minHeightDps;
+    private String name;
+    private float minWidthDps;
+    private float minHeightDps;
 
     /**
      * Number of icons per row and column in the workspace.
@@ -95,9 +98,11 @@
 
     public Point defaultWallpaperSize;
 
+    private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>();
+    private ConfigMonitor mConfigMonitor;
+
     @VisibleForTesting
-    public InvariantDeviceProfile() {
-    }
+    public InvariantDeviceProfile() {}
 
     private InvariantDeviceProfile(InvariantDeviceProfile p) {
         this(p.name, p.minWidthDps, p.minHeightDps, p.numRows, p.numColumns,
@@ -125,6 +130,12 @@
 
     @TargetApi(23)
     private InvariantDeviceProfile(Context context) {
+        initGrid(context);
+        mConfigMonitor = new ConfigMonitor(context,
+                APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
+    }
+
+    private void initGrid(Context context) {
         WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
         Display display = wm.getDefaultDisplay();
         DisplayMetrics dm = new DisplayMetrics();
@@ -185,6 +196,44 @@
         }
     }
 
+    public void addOnChangeListener(OnIDPChangeListener listener) {
+        mChangeListeners.add(listener);
+    }
+
+    private void killProcess(Context context) {
+        Log.e("ConfigMonitor", "restarting launcher");
+        android.os.Process.killProcess(android.os.Process.myPid());
+    }
+
+    private void onConfigChanged(Context context) {
+        // Config changes, what shall we do?
+        InvariantDeviceProfile oldProfile = new InvariantDeviceProfile(this);
+
+        // Re-init grid
+        initGrid(context);
+
+        int changeFlags = 0;
+        if (numRows != oldProfile.numRows ||
+                numColumns != oldProfile.numColumns ||
+                numFolderColumns != oldProfile.numFolderColumns ||
+                numFolderRows != oldProfile.numFolderRows ||
+                numHotseatIcons != oldProfile.numHotseatIcons) {
+            changeFlags |= CHANGE_FLAG_GRID;
+        }
+
+        if (iconSize != oldProfile.iconSize || iconBitmapSize != oldProfile.iconBitmapSize) {
+            changeFlags |= CHANGE_FLAG_ICON_SIZE;
+        }
+
+        // Create a new config monitor
+        mConfigMonitor.unregister();
+        mConfigMonitor = new ConfigMonitor(context, this::onConfigChanged);
+
+        for (OnIDPChangeListener listener : mChangeListeners) {
+            listener.onIdpChanged(changeFlags, this);
+        }
+    }
+
     ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles(Context context) {
         ArrayList<InvariantDeviceProfile> profiles = new ArrayList<>();
         try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
@@ -356,4 +405,8 @@
         return x * aspectRatio + y;
     }
 
+    public interface OnIDPChangeListener {
+
+        void onIdpChanged(int changeFlags, InvariantDeviceProfile profile);
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 6bf5812..338c20b 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
+import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_SIZE;
 
 import android.content.ComponentName;
 import android.content.ContentProviderClient;
@@ -30,6 +31,7 @@
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Preconditions;
@@ -94,6 +96,7 @@
 
         mContext.registerReceiver(mModel, filter);
         UserManagerCompat.getInstance(mContext).enableAndResetCache();
+        mInvariantDeviceProfile.addOnChangeListener(this::onIdpChanged);
 
         if (!mContext.getResources().getBoolean(R.bool.notification_badging_enabled)) {
             mNotificationBadgingObserver = null;
@@ -113,6 +116,19 @@
         }
     }
 
+    private void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
+        if (changeFlags == 0) {
+            return;
+        }
+
+        if ((changeFlags & CHANGE_FLAG_ICON_SIZE) != 0) {
+            LauncherIcons.clearPool();
+            mIconCache.updateIconParams(idp.fillResIconDpi, idp.iconBitmapSize);
+        }
+
+        mModel.forceReload();
+    }
+
     /**
      * Call from Application.onTerminate(), which is not guaranteed to ever be called.
      */
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index dc60c8f..290d8c4 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -84,6 +84,12 @@
     // trying to make them fit the orientation the device is in.
     public static final boolean OVERVIEW_USE_SCREENSHOT_ORIENTATION = true;
 
+    /**
+     * Feature flag to handle define config changes dynamically instead of killing the process.
+     */
+    public static final TogglableFlag APPLY_CONFIG_AT_RUNTIME = new TogglableFlag(
+            "APPLY_CONFIG_AT_RUNTIME", false, "Apply display changes dynamically");
+
     public static void initialize(Context context) {
         // Avoid the disk read for builds without the flags UI.
         if (showFlagTogglerUi()) {
@@ -188,5 +194,4 @@
             return h$;
         }
     }
-
 }
diff --git a/src/com/android/launcher3/icons/BaseIconCache.java b/src/com/android/launcher3/icons/BaseIconCache.java
index 6433103..5b74d1d 100644
--- a/src/com/android/launcher3/icons/BaseIconCache.java
+++ b/src/com/android/launcher3/icons/BaseIconCache.java
@@ -90,11 +90,11 @@
     private final HashMap<ComponentKey, CacheEntry> mCache =
             new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
     private final InstantAppResolver mInstantAppResolver;
-    final int mIconDpi;
-
-    final IconDB mIconDb;
     final Handler mWorkerHandler;
 
+    int mIconDpi;
+    IconDB mIconDb;
+
     private final BitmapFactory.Options mDecodeOptions;
 
     public BaseIconCache(Context context, int iconDpi, int iconPixelSize) {
@@ -103,8 +103,6 @@
         mUserManager = UserManagerCompat.getInstance(mContext);
         mLauncherApps = LauncherAppsCompat.getInstance(mContext);
         mInstantAppResolver = InstantAppResolver.newInstance(mContext);
-        mIconDpi = iconDpi;
-        mIconDb = new IconDB(context, iconPixelSize);
 
         mIconProvider = IconProvider.newInstance(context);
         mWorkerHandler = new Handler(LauncherModel.getWorkerLooper());
@@ -115,6 +113,22 @@
         } else {
             mDecodeOptions = null;
         }
+
+        mIconDpi = iconDpi;
+        mIconDb = new IconDB(context, iconPixelSize);
+    }
+
+    public void updateIconParams(int iconDpi, int iconPixelSize) {
+        mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
+    }
+
+    private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) {
+        mIconDpi = iconDpi;
+        mDefaultIcons.clear();
+
+        mIconDb.close();
+        mIconDb = new IconDB(mContext, iconPixelSize);
+        mCache.clear();
     }
 
     private Drawable getFullResDefaultActivityIcon() {
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 69614a3..c96d35d 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -27,11 +27,11 @@
 
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.graphics.BitmapRenderer;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.graphics.BitmapRenderer;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutInfoCompat;
@@ -48,13 +48,14 @@
 
     private static final Object sPoolSync = new Object();
     private static LauncherIcons sPool;
-    private LauncherIcons next;
+    private static int sPoolId = 0;
 
     /**
      * Return a new Message instance from the global pool. Allows us to
      * avoid allocating new objects in many cases.
      */
     public static LauncherIcons obtain(Context context) {
+        int poolId;
         synchronized (sPoolSync) {
             if (sPool != null) {
                 LauncherIcons m = sPool;
@@ -62,9 +63,33 @@
                 m.next = null;
                 return m;
             }
+            poolId = sPoolId;
         }
+
         InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
-        return new LauncherIcons(context, idp.fillResIconDpi, idp.iconBitmapSize);
+        return new LauncherIcons(context, idp.fillResIconDpi, idp.iconBitmapSize, poolId);
+    }
+
+    public static void clearPool() {
+        synchronized (sPoolSync) {
+            sPool = null;
+            sPoolId++;
+        }
+    }
+
+    private final Context mContext;
+    private final int mFillResIconDpi;
+    private final int mIconBitmapSize;
+    private final int mPoolId;
+
+    private LauncherIcons next;
+
+    private LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize, int poolId) {
+        super(context, fillResIconDpi, iconBitmapSize);
+        mContext = context.getApplicationContext();
+        mFillResIconDpi = fillResIconDpi;
+        mIconBitmapSize = iconBitmapSize;
+        mPoolId = poolId;
     }
 
     /**
@@ -72,6 +97,9 @@
      */
     public void recycle() {
         synchronized (sPoolSync) {
+            if (sPoolId != mPoolId) {
+                return;
+            }
             // Clear any temporary state variables
             clear();
 
@@ -85,17 +113,6 @@
         recycle();
     }
 
-    private final Context mContext;
-    private final int mFillResIconDpi;
-    private final int mIconBitmapSize;
-
-    private LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize) {
-        super(context, fillResIconDpi, iconBitmapSize);
-        mContext = context.getApplicationContext();
-        mFillResIconDpi = fillResIconDpi;
-        mIconBitmapSize = iconBitmapSize;
-    }
-
     public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
             int iconAppTargetSdk) {
         return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false);
diff --git a/src/com/android/launcher3/util/ConfigMonitor.java b/src/com/android/launcher3/util/ConfigMonitor.java
index 5dd0d08..717acdc 100644
--- a/src/com/android/launcher3/util/ConfigMonitor.java
+++ b/src/com/android/launcher3/util/ConfigMonitor.java
@@ -29,9 +29,12 @@
 import android.view.Display;
 import android.view.WindowManager;
 
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.Utilities.Consumer;
+
 /**
  * {@link BroadcastReceiver} which watches configuration changes and
- * restarts the process in case changes which affect the device profile occur.
+ * notifies the callback in case changes which affect the device profile occur.
  */
 public class ConfigMonitor extends BroadcastReceiver implements DisplayListener {
 
@@ -48,7 +51,9 @@
     private final Point mRealSize;
     private final Point mSmallestSize, mLargestSize;
 
-    public ConfigMonitor(Context context) {
+    private Consumer<Context> mCallback;
+
+    public ConfigMonitor(Context context, Consumer<Context> callback) {
         mContext = context;
 
         Configuration config = context.getResources().getConfiguration();
@@ -64,6 +69,12 @@
         mSmallestSize = new Point();
         mLargestSize = new Point();
         display.getCurrentSizeRange(mSmallestSize, mLargestSize);
+
+        mCallback = callback;
+
+        mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
+        mContext.getSystemService(DisplayManager.class)
+                .registerDisplayListener(this, new Handler(UiThreadHelper.getBackgroundLooper()));
     }
 
     @Override
@@ -71,16 +82,10 @@
         Configuration config = context.getResources().getConfiguration();
         if (mFontScale != config.fontScale || mDensity != config.densityDpi) {
             Log.d(TAG, "Configuration changed");
-            killProcess();
+            notifyChange();
         }
     }
 
-    public void register() {
-        mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
-        mContext.getSystemService(DisplayManager.class)
-                .registerDisplayListener(this, new Handler(UiThreadHelper.getBackgroundLooper()));
-    }
-
     @Override
     public void onDisplayAdded(int displayId) { }
 
@@ -97,7 +102,7 @@
 
         if (!mRealSize.equals(mTmpPoint1) && !mRealSize.equals(mTmpPoint1.y, mTmpPoint1.x)) {
             Log.d(TAG, String.format("Display size changed from %s to %s", mRealSize, mTmpPoint1));
-            killProcess();
+            notifyChange();
             return;
         }
 
@@ -105,22 +110,28 @@
         if (!mSmallestSize.equals(mTmpPoint1) || !mLargestSize.equals(mTmpPoint2)) {
             Log.d(TAG, String.format("Available size changed from [%s, %s] to [%s, %s]",
                     mSmallestSize, mLargestSize, mTmpPoint1, mTmpPoint2));
-            killProcess();
+            notifyChange();
         }
     }
 
-    private void killProcess() {
-        Log.d(TAG, "restarting launcher");
-        try {
-            mContext.unregisterReceiver(this);
-            mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
-        } catch (Exception e) {
-            // We are going to die anyway, ignore any error die to race condition in registering.
+    private synchronized void notifyChange() {
+        if (mCallback != null) {
+            Consumer<Context> callback = mCallback;
+            mCallback = null;
+            new MainThreadExecutor().execute(() -> callback.accept(mContext));
         }
-        android.os.Process.killProcess(android.os.Process.myPid());
     }
 
     private Display getDefaultDisplay(Context context) {
         return context.getSystemService(WindowManager.class).getDefaultDisplay();
     }
+
+    public void unregister() {
+        try {
+            mContext.unregisterReceiver(this);
+            mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to unregister config monitor", e);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/util/SQLiteCacheHelper.java b/src/com/android/launcher3/util/SQLiteCacheHelper.java
index 44c1762..3faf070 100644
--- a/src/com/android/launcher3/util/SQLiteCacheHelper.java
+++ b/src/com/android/launcher3/util/SQLiteCacheHelper.java
@@ -87,6 +87,10 @@
         mOpenHelper.clearDB(mOpenHelper.getWritableDatabase());
     }
 
+    public void close() {
+        mOpenHelper.close();
+    }
+
     protected abstract void onCreateTable(SQLiteDatabase db);
 
     /**