Adding support for dynamically change icon shape for AdaptiveIcons

> This would allow developers to verify their icon designs on different
device configurations
> This settings is only visible when developer settings is enabled

Change-Id: I7e32abfede001c134f23390734dcd39c93b68b9a
diff --git a/res/values/config.xml b/res/values/config.xml
index 745bce3..8366d73 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -13,6 +13,26 @@
      easily override the app name without providing all translations -->
     <string name="derived_app_name" translatable="false">@string/app_name</string>
 
+    <!-- Values for icon shape overrides. These should correspond to entries defined
+     in icon_shape_override_paths_names -->
+    <string-array name="icon_shape_override_paths_values">
+        <item></item>
+        <item>M50,0L100,0 100,100 0,100 0,0z</item>
+        <item>M50,0L80,0 A20,20,0,0 1 100,20 L100,80 A20,20,0,0 1 80,100 L20,100 A20,20,0,0 1 0,80 L 0,20 A20,20,0,0 1 20,0z</item>
+        <item>M50,0 C10,0 0,10 0,50 0,90 10,100 50,100 90,100 100,90 100,50 100,10 90,0 50,0 Z</item>
+        <item>M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0</item>
+        <item>M50,0A50,30 0,0,1 100,30V70A50,30 0,0,1 0,70V30A50,30 0,0,1 50,0z</item>
+    </string-array>
+
+    <string-array name="icon_shape_override_paths_names">
+        <!-- Option to not change the icon shape on home screen. [CHAR LIMIT=50] -->
+        <item>@string/icon_shape_no_override</item>
+        <item translatable="false">Square</item>
+        <item translatable="false">Rounded corner rect</item>
+        <item translatable="false">Squircle</item>
+        <item translatable="false">Circle</item>
+        <item translatable="false">Cylinder</item>
+    </string-array>
 <!-- DragController -->
     <item type="id" name="drag_event_parity" />
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 99ff581..0461e4a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -184,6 +184,13 @@
     <!-- Text description of the setting that allows the automatic placement of launcher shortcuts for applications and games installed on the device [CHAR LIMIT=NONE] -->
     <string name="auto_add_shortcuts_description">For new apps</string>
 
+    <!-- Developer setting to change the shape of icons on home screen. [CHAR LIMIT=50] -->
+    <string name="icon_shape_override_label">Change icon shape</string>
+    <!-- Option to not change the icon shape on home screen. [CHAR LIMIT=50] -->
+    <string name="icon_shape_no_override">Do not change</string>
+    <!-- Message shown in the progress dialog when the icon shape override is being applied [CHAR LIMIT=100]-->
+    <string name="icon_shape_override_progress">Applying icon shape changes</string>
+
     <!-- Label on an icon that references an uninstalled package, for which we have no information about when it might be installed. [CHAR_LIMIT=15] -->
     <string name="package_state_unknown">Unknown</string>
 
diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml
index a16583d..301bef1 100644
--- a/res/xml/launcher_preferences.xml
+++ b/res/xml/launcher_preferences.xml
@@ -30,4 +30,14 @@
         android:defaultValue="true"
         android:persistent="true"
         />
+
+    <ListPreference
+        android:key="pref_override_icon_shape"
+        android:title="@string/icon_shape_override_label"
+        android:summary="%s"
+        android:entries="@array/icon_shape_override_paths_names"
+        android:entryValues="@array/icon_shape_override_paths_values"
+        android:defaultValue=""
+        android:persistent="false" />
+
 </PreferenceScreen>
diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java
index 9bf2809..924b79b 100644
--- a/src/com/android/launcher3/IconCache.java
+++ b/src/com/android/launcher3/IconCache.java
@@ -572,6 +572,11 @@
         return entry;
     }
 
+    public synchronized void clear() {
+        Preconditions.assertWorkerThread();
+        mIconDb.clear();
+    }
+
     /**
      * Adds a default package entry in the cache. This entry is not persisted and will be removed
      * when the cache is flushed.
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index a750406..4771649 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -55,6 +55,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.config.ProviderConfig;
 import com.android.launcher3.dynamicui.ExtractionUtils;
+import com.android.launcher3.graphics.IconShapeOverride;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.provider.RestoreDbTask;
@@ -96,6 +97,7 @@
         // is the first component to get created. Initializing FileLog here ensures that it's
         // always available in the main process.
         FileLog.setDir(getContext().getApplicationContext().getFilesDir());
+        IconShapeOverride.apply(getContext());
         return true;
     }
 
diff --git a/src/com/android/launcher3/SettingsActivity.java b/src/com/android/launcher3/SettingsActivity.java
index 552e24a..7ae6b26 100644
--- a/src/com/android/launcher3/SettingsActivity.java
+++ b/src/com/android/launcher3/SettingsActivity.java
@@ -21,12 +21,15 @@
 import android.database.ContentObserver;
 import android.os.Bundle;
 import android.os.Handler;
+import android.preference.ListPreference;
 import android.preference.Preference;
 import android.preference.PreferenceFragment;
 import android.provider.Settings;
 import android.provider.Settings.System;
 import android.support.v4.os.BuildCompat;
 
+import com.android.launcher3.graphics.IconShapeOverride;
+
 /**
  * Settings activity for Launcher. Currently implements the following setting: Allow rotation
  */
@@ -78,6 +81,15 @@
                 getPreferenceScreen().removePreference(
                         findPreference(SessionCommitReceiver.ADD_ICON_PREFERENCE_KEY));
             }
+
+            Preference iconShapeOverride = findPreference(IconShapeOverride.KEY_PREFERENCE);
+            if (iconShapeOverride != null) {
+                if (IconShapeOverride.isSupported(getActivity())) {
+                    IconShapeOverride.handlePreferenceUi((ListPreference) iconShapeOverride);
+                } else {
+                    getPreferenceScreen().removePreference(iconShapeOverride);
+                }
+            }
         }
 
         @Override
diff --git a/src/com/android/launcher3/graphics/IconShapeOverride.java b/src/com/android/launcher3/graphics/IconShapeOverride.java
new file mode 100644
index 0000000..6e4d366
--- /dev/null
+++ b/src/com/android/launcher3/graphics/IconShapeOverride.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2017 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.launcher3.graphics;
+
+import android.annotation.TargetApi;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.SystemClock;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherFiles;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.LooperExecuter;
+
+import java.lang.reflect.Field;
+
+/**
+ * Utility class to override shape of {@link android.graphics.drawable.AdaptiveIconDrawable}.
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class IconShapeOverride {
+
+    private static final String TAG = "IconShapeOverride";
+
+    public static final String KEY_PREFERENCE = "pref_override_icon_shape";
+
+    // Time to wait before killing the process this ensures that the progress bar is visible for
+    // sufficient time so that there is no flicker.
+    private static final long PROCESS_KILL_DELAY_MS = 1000;
+
+    private static final int RESTART_REQUEST_CODE = 42; // the answer to everything
+
+    public static boolean isSupported(Context context) {
+        if (!Utilities.isAtLeastO()) {
+            return false;
+        }
+        // Only supported when developer settings is enabled
+        if (Settings.Global.getInt(context.getContentResolver(),
+                Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 1) {
+            return false;
+        }
+
+        try {
+            if (getSystemResField().get(null) != Resources.getSystem()) {
+                // Our assumption that mSystem is the system resource is not true.
+                return false;
+            }
+        } catch (Exception e) {
+            // Ignore, not supported
+            return false;
+        }
+
+        return getConfigResId() != 0;
+    }
+
+    public static void apply(Context context) {
+        if (!Utilities.isAtLeastO()) {
+            return;
+        }
+        String path = getAppliedValue(context);
+        if (TextUtils.isEmpty(path)) {
+            return;
+        }
+        if (!isSupported(context)) {
+            return;
+        }
+
+        // magic
+        try {
+            Resources override =
+                    new ResourcesOverride(Resources.getSystem(), getConfigResId(), path);
+            getSystemResField().set(null, override);
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to override icon shape", e);
+            // revert value.
+            prefs(context).edit().remove(KEY_PREFERENCE).apply();
+        }
+    }
+
+    private static Field getSystemResField() throws Exception {
+        Field staticField = Resources.class.getDeclaredField("mSystem");
+        staticField.setAccessible(true);
+        return staticField;
+    }
+
+    private static int getConfigResId() {
+        return Resources.getSystem().getIdentifier("config_icon_mask", "string", "android");
+    }
+
+    private static String getAppliedValue(Context context) {
+        return prefs(context).getString(KEY_PREFERENCE, "");
+    }
+
+    private static SharedPreferences prefs(Context context) {
+        return context.getSharedPreferences(LauncherFiles.DEVICE_PREFERENCES_KEY, 0);
+    }
+
+    public static void handlePreferenceUi(ListPreference preference) {
+        Context context = preference.getContext();
+        preference.setValue(getAppliedValue(context));
+        preference.setOnPreferenceChangeListener(new PreferenceChangeHandler(context));
+    }
+
+    private static class ResourcesOverride extends Resources {
+
+        private final int mOverrideId;
+        private final String mOverrideValue;
+
+        @SuppressWarnings("deprecated")
+        public ResourcesOverride(Resources parent, int overrideId, String overrideValue) {
+            super(parent.getAssets(), parent.getDisplayMetrics(), parent.getConfiguration());
+            mOverrideId = overrideId;
+            mOverrideValue = overrideValue;
+        }
+
+        @NonNull
+        @Override
+        public String getString(int id) throws NotFoundException {
+            if (id == mOverrideId) {
+                return mOverrideValue;
+            }
+            return super.getString(id);
+        }
+    }
+
+    private static class PreferenceChangeHandler implements OnPreferenceChangeListener {
+
+        private final Context mContext;
+
+        private PreferenceChangeHandler(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public boolean onPreferenceChange(Preference preference, Object o) {
+            String newValue = (String) o;
+            if (!getAppliedValue(mContext).equals(newValue)) {
+                // Value has changed
+                ProgressDialog.show(mContext,
+                        null /* title */,
+                        mContext.getString(R.string.icon_shape_override_progress),
+                        true /* indeterminate */,
+                        false /* cancelable */);
+                new LooperExecuter(LauncherModel.getWorkerLooper()).execute(
+                        new OverrideApplyHandler(mContext, newValue));
+            }
+            return false;
+        }
+    }
+
+    private static class OverrideApplyHandler implements Runnable {
+
+        private final Context mContext;
+        private final String mValue;
+
+        private OverrideApplyHandler(Context context, String value) {
+            mContext = context;
+            mValue = value;
+        }
+
+        @Override
+        public void run() {
+            // Synchronously write the preference.
+            prefs(mContext).edit().putString(KEY_PREFERENCE, mValue).commit();
+            // Clear the icon cache.
+            LauncherAppState.getInstance(mContext).getIconCache().clear();
+
+            // Wait for it
+            try {
+                Thread.sleep(PROCESS_KILL_DELAY_MS);
+            } catch (Exception e) {
+                Log.e(TAG, "Error waiting", e);
+            }
+
+            // Schedule an alarm before we kill ourself.
+            Intent homeIntent = new Intent(Intent.ACTION_MAIN)
+                    .addCategory(Intent.CATEGORY_HOME)
+                    .setPackage(mContext.getPackageName())
+                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            PendingIntent pi = PendingIntent.getActivity(mContext, RESTART_REQUEST_CODE,
+                    homeIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
+            mContext.getSystemService(AlarmManager.class).setExact(
+                    AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 50, pi);
+
+            // Kill process
+            android.os.Process.killProcess(android.os.Process.myPid());
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/SQLiteCacheHelper.java b/src/com/android/launcher3/util/SQLiteCacheHelper.java
index 1ff6293..ef10f97 100644
--- a/src/com/android/launcher3/util/SQLiteCacheHelper.java
+++ b/src/com/android/launcher3/util/SQLiteCacheHelper.java
@@ -83,6 +83,10 @@
                 mTableName, columns, selection, selectionArgs, null, null, null);
     }
 
+    public void clear() {
+        mOpenHelper.clearDB(mOpenHelper.getWritableDatabase());
+    }
+
     protected abstract void onCreateTable(SQLiteDatabase db);
 
     /**