From ca18746163621211847a2f184d19a6b3e2b4a1c0 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Fri, 31 Mar 2017 20:09:34 -0700 Subject: 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 --- res/values/config.xml | 20 ++ res/values/strings.xml | 7 + res/xml/launcher_preferences.xml | 10 + src/com/android/launcher3/IconCache.java | 5 + src/com/android/launcher3/LauncherProvider.java | 2 + src/com/android/launcher3/SettingsActivity.java | 12 ++ .../launcher3/graphics/IconShapeOverride.java | 217 +++++++++++++++++++++ .../android/launcher3/util/SQLiteCacheHelper.java | 4 + 8 files changed, 277 insertions(+) create mode 100644 src/com/android/launcher3/graphics/IconShapeOverride.java diff --git a/res/values/config.xml b/res/values/config.xml index 745bce3f5..8366d73fc 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/app_name + + + + M50,0L100,0 100,100 0,100 0,0z + 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 + 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 + M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0 + M50,0A50,30 0,0,1 100,30V70A50,30 0,0,1 0,70V30A50,30 0,0,1 50,0z + + + + + @string/icon_shape_no_override + Square + Rounded corner rect + Squircle + Circle + Cylinder + diff --git a/res/values/strings.xml b/res/values/strings.xml index 99ff581e8..0461e4a02 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -184,6 +184,13 @@ For new apps + + Change icon shape + + Do not change + + Applying icon shape changes + Unknown diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml index a16583d2c..301bef104 100644 --- a/res/xml/launcher_preferences.xml +++ b/res/xml/launcher_preferences.xml @@ -30,4 +30,14 @@ android:defaultValue="true" android:persistent="true" /> + + + diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index 9bf2809cb..924b79be0 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -572,6 +572,11 @@ public class IconCache { 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 a75040657..4771649d5 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -55,6 +55,7 @@ import com.android.launcher3.compat.UserManagerCompat; 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 @@ public class LauncherProvider extends ContentProvider { // 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 552e24ae4..7ae6b261d 100644 --- a/src/com/android/launcher3/SettingsActivity.java +++ b/src/com/android/launcher3/SettingsActivity.java @@ -21,12 +21,15 @@ import android.content.ContentResolver; 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 @@ public class SettingsActivity extends Activity { 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 000000000..6e4d36642 --- /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 1ff6293a0..ef10f97fb 100644 --- a/src/com/android/launcher3/util/SQLiteCacheHelper.java +++ b/src/com/android/launcher3/util/SQLiteCacheHelper.java @@ -83,6 +83,10 @@ public abstract class SQLiteCacheHelper { mTableName, columns, selection, selectionArgs, null, null, null); } + public void clear() { + mOpenHelper.clearDB(mOpenHelper.getWritableDatabase()); + } + protected abstract void onCreateTable(SQLiteDatabase db); /** -- cgit v1.2.3