diff options
author | Joey <joey@lineageos.org> | 2020-08-05 15:52:24 +0200 |
---|---|---|
committer | Joey <joey@lineageos.org> | 2021-02-17 10:40:44 +0100 |
commit | 7f328bb343c6bdc68d6036f91a2f56a69098162b (patch) | |
tree | d9e8f9188e7636fdbd43ef3af3e590417e0b544a | |
parent | 94afb12f286e5a92ebb4c653dd5cb4a54d7b31aa (diff) |
Launcher3: Add support for icon packs
- Supports icon packs from nova and adw
- Live preview in settings
- Default applied icon pack via overlay
Change-Id: Ie9879fa30f4cd9d52356acf78a423b415657510c
Signed-off-by: Joey <joey@lineageos.org>
24 files changed, 1578 insertions, 4 deletions
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index 4e4ebf993..2d4e1cfff 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -174,6 +174,12 @@ </intent-filter> </activity> + <activity + android:name="com.android.launcher3.lineage.icon.IconPackSettingsActivity" + android:label="@string/icon_pack_title" + android:theme="@android:style/Theme.DeviceDefault.Settings" + android:autoRemoveFromRecents="true" /> + <provider android:name="com.android.launcher3.testing.TestInformationProvider" android:authorities="${packageName}.TestInfo" diff --git a/res/drawable/ic_icon_pack_plus.xml b/res/drawable/ic_icon_pack_plus.xml new file mode 100644 index 000000000..2686fe476 --- /dev/null +++ b/res/drawable/ic_icon_pack_plus.xml @@ -0,0 +1,26 @@ +<!-- + Copyright (C) 2019 The LineageOS 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. +--> +<vector + xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M 19 3 L 5 3 C 3.89 3 3 3.9 3 5 L 3 19 C 3 20.1 3.89 21 5 21 L 19 21 C 20.1 21 21 20.1 21 19 L 21 5 C 21 3.9 20.1 3 19 3 Z M 19 19 L 5 19 L 5 5 L 19 5 L 19 19 Z M 11 17 L 13 17 L 13 13 L 17 13 L 17 11 L 13 11 L 13 7 L 11 7 L 11 11 L 7 11 L 7 13 L 11 13 Z" + android:fillColor="?android:attr/textColorPrimary" + android:strokeWidth="1"/> +</vector> diff --git a/res/layout/preference_radio.xml b/res/layout/preference_radio.xml new file mode 100644 index 000000000..9cdacad25 --- /dev/null +++ b/res/layout/preference_radio.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> +<!-- This file is copied from preference_app.xml with modification to + support widget on the opposite side horizontally --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:settings="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/selectableItemBackground" + android:gravity="center_vertical" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <LinearLayout + android:id="@android:id/widget_frame" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center" + android:minWidth="56dp" + android:layout_marginEnd="16dp" + android:orientation="vertical" /> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical" + android:paddingTop="16dp" + android:paddingBottom="16dp"> + + <TextView android:id="@android:id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:singleLine="true" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Subhead" + android:ellipsize="marquee" + android:fadingEdge="horizontal" /> + + <LinearLayout + android:id="@+id/summary_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:visibility="gone"> + <TextView android:id="@android:id/summary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small" + android:textAlignment="viewStart" + android:textColor="?android:attr/textColorSecondary" /> + + <TextView android:id="@+id/appendix" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small" + android:textAlignment="viewEnd" + android:textColor="?android:attr/textColorSecondary" + android:maxLines="1" + android:ellipsize="end" /> + </LinearLayout> + </LinearLayout> +</LinearLayout> diff --git a/res/layout/preference_widget_icons_preview.xml b/res/layout/preference_widget_icons_preview.xml new file mode 100644 index 000000000..c87819b44 --- /dev/null +++ b/res/layout/preference_widget_icons_preview.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 Shift GmbH + + 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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:settings="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="horizontal" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:paddingBottom="24dp" + android:paddingTop="24dp" + android:paddingStart="?android:attr/listPreferredItemPaddingEnd" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <ImageView + android:id="@+id/pref_icon_a" + android:layout_height="56dp" + android:layout_width="56dp" + android:layout_marginHorizontal="16dp" /> + <ImageView + android:id="@+id/pref_icon_b" + android:layout_height="56dp" + android:layout_width="56dp" + android:layout_marginHorizontal="16dp" /> + <ImageView + android:id="@+id/pref_icon_c" + android:layout_height="56dp" + android:layout_width="56dp" + android:layout_marginHorizontal="16dp" /> + <ImageView + android:id="@+id/pref_icon_d" + android:layout_height="56dp" + android:layout_width="56dp" + android:layout_marginHorizontal="16dp" /> +</LinearLayout> diff --git a/res/layout/preference_widget_radiobutton.xml b/res/layout/preference_widget_radiobutton.xml new file mode 100644 index 000000000..c25ed17b6 --- /dev/null +++ b/res/layout/preference_widget_radiobutton.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2006 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. +--> +<!-- Layout used by CheckBoxPreference for the checkbox style. This is inflated + inside android.R.layout.preference. --> +<RadioButton xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="@null" + android:focusable="false" + android:clickable="false" /> diff --git a/res/menu/menu_icon_pack.xml b/res/menu/menu_icon_pack.xml new file mode 100644 index 000000000..30e04fae2 --- /dev/null +++ b/res/menu/menu_icon_pack.xml @@ -0,0 +1,23 @@ +<!-- + Copyright (C) 2019 The LineageOS 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/menu_icon_pack" + android:icon="@drawable/ic_icon_pack_plus" + android:showAsAction="always" + android:title="@string/icon_pack_add" /> +</menu> diff --git a/res/values/lineage_config.xml b/res/values/lineage_config.xml index 7067ca880..db8620f6c 100644 --- a/res/values/lineage_config.xml +++ b/res/values/lineage_config.xml @@ -16,4 +16,9 @@ <resources> <string name="pref_show_google_now_summary" translatable="false">@string/msg_minus_one_on_left</string> + + <!-- Icon pack --> + <string name="icon_pack_settings_class" translatable="false">com.android.launcher3.lineage.icon.IconPackSettingsFragment</string> + <!-- Default icon pack package. Set to "android" to use default / original icons --> + <string name="icon_pack_default_pkg" translatable="false">android</string> </resources> diff --git a/res/values/lineage_strings.xml b/res/values/lineage_strings.xml index 2e602ffb4..506d9bf30 100644 --- a/res/values/lineage_strings.xml +++ b/res/values/lineage_strings.xml @@ -49,4 +49,10 @@ <string name="trust_apps_help">Help</string> <string name="trust_apps_info_hidden">Hidden apps and their widgets are hidden from the drawer</string> <string name="trust_apps_info_protected">Protected apps require authentication to be opened from the launcher</string> + + <!-- Icon pack --> + <string name="icon_pack_title">Icon pack</string> + <string name="icon_pack_default_label">Default</string> + <string name="icon_pack_add">Install more</string> + <string name="icon_pack_no_market">There\'s no available app store</string> </resources> diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml index b9801fe4d..36eb21363 100644 --- a/res/xml/launcher_preferences.xml +++ b/res/xml/launcher_preferences.xml @@ -62,6 +62,10 @@ android:key="pref_trust_apps" android:title="@string/trust_apps_manager_name" /> + <Preference + android:key="pref_icon_pack" + android:title="@string/icon_pack_title" /> + <androidx.preference.PreferenceScreen android:key="pref_developer_options" android:persistent="false" diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index 02f9b8ff3..16e998390 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -49,6 +49,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.launcher3.graphics.IconShape; +import com.android.launcher3.lineage.icon.IconPackStore; import com.android.launcher3.util.ConfigMonitor; import com.android.launcher3.util.DefaultDisplay; import com.android.launcher3.util.DefaultDisplay.Info; @@ -109,6 +110,7 @@ public class InvariantDeviceProfile implements OnSharedPreferenceChangeListener public int numFolderRows; public int numFolderColumns; public float iconSize; + public String iconPack; public String iconShapePath; public float landscapeIconSize; public int iconBitmapSize; @@ -154,6 +156,7 @@ public class InvariantDeviceProfile implements OnSharedPreferenceChangeListener numFolderRows = p.numFolderRows; numFolderColumns = p.numFolderColumns; iconSize = p.iconSize; + iconPack = p.iconPack; iconShapePath = p.iconShapePath; landscapeIconSize = p.landscapeIconSize; iconBitmapSize = p.iconBitmapSize; @@ -277,6 +280,7 @@ public class InvariantDeviceProfile implements OnSharedPreferenceChangeListener iconSize = displayOption.iconSize; iconShapePath = getIconShapePath(context); + iconPack = new IconPackStore(context).getCurrent(); landscapeIconSize = displayOption.landscapeIconSize; iconBitmapSize = ResourceUtils.pxFromDp(iconSize, displayInfo.metrics); iconTextSize = displayOption.iconTextSize; @@ -376,7 +380,8 @@ public class InvariantDeviceProfile implements OnSharedPreferenceChangeListener } if (iconSize != oldProfile.iconSize || iconBitmapSize != oldProfile.iconBitmapSize || - !iconShapePath.equals(oldProfile.iconShapePath)) { + !iconShapePath.equals(oldProfile.iconShapePath) || + !iconPack.equals(oldProfile.iconPack)) { changeFlags |= CHANGE_FLAG_ICON_PARAMS; } if (!iconShapePath.equals(oldProfile.iconShapePath)) { diff --git a/src/com/android/launcher3/icons/IconProvider.java b/src/com/android/launcher3/icons/IconProvider.java index 1468b2782..ab4edb9e1 100644 --- a/src/com/android/launcher3/icons/IconProvider.java +++ b/src/com/android/launcher3/icons/IconProvider.java @@ -38,6 +38,8 @@ import com.android.launcher3.icons.BitmapInfo.Extender; import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.SafeCloseable; +import com.android.launcher3.lineage.icon.IconPack; +import com.android.launcher3.lineage.icon.providers.IconPackProvider; import java.util.Calendar; import java.util.function.BiConsumer; @@ -125,7 +127,11 @@ public class IconProvider { && Process.myUserHandle().equals(user)) { icon = loadClockDrawable(0); } - return icon == null ? loader.apply(obj, param) : icon; + icon = getFromIconPack(icon, packageName); + if (icon == null) { + icon = loader.apply(obj, param); + } + return icon; } private Drawable loadCalendarDrawable(int iconDpi) { @@ -248,4 +254,14 @@ public class IconProvider { return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn); } + + private Drawable getFromIconPack(Drawable icon, String packageName) { + final IconPack iconPack = IconPackProvider.loadAndGetIconPack(mContext); + if (iconPack == null) { + return null; + } + + final Drawable iconMask = iconPack.getIcon(packageName, null, ""); + return iconMask == null ? icon : iconMask; + } } diff --git a/src/com/android/launcher3/lineage/icon/GetLaunchableInfoTask.java b/src/com/android/launcher3/lineage/icon/GetLaunchableInfoTask.java new file mode 100644 index 000000000..824339baa --- /dev/null +++ b/src/com/android/launcher3/lineage/icon/GetLaunchableInfoTask.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 The LineageOS 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.lineage.icon; + +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.LauncherActivityInfo; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Process; +import android.os.UserHandle; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +final class GetLaunchableInfoTask extends AsyncTask<Void, Void, List<LauncherActivityInfo>> { + + private final PackageManager pm; + private final LauncherApps launcherApps; + private final int limit; + private final Callback callback; + + GetLaunchableInfoTask(PackageManager pm, + LauncherApps launcherApps, + int limit, + Callback callback) { + this.pm = pm; + this.launcherApps = launcherApps; + this.limit = limit; + this.callback = callback; + } + + @Override + protected List<LauncherActivityInfo> doInBackground(Void... voids) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + // This should never happen + return new ArrayList<>(); + } + + final UserHandle ua = Process.myUserHandle(); + final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null) + .addCategory(Intent.CATEGORY_LAUNCHER); + return pm.queryIntentActivities(mainIntent, 0) + .parallelStream() + .sorted(new ResolveInfo.DisplayNameComparator(pm)) + .limit(limit) + .map((ri) -> { + final ActivityInfo ai = ri.activityInfo; + final Intent i = new Intent(); + i.setClassName(ai.applicationInfo.packageName, ai.name); + return launcherApps.resolveActivity(i, ua); + }) + .collect(Collectors.toList()); + } + + @Override + protected void onPostExecute(List<LauncherActivityInfo> list) { + callback.onLoadCompleted(list); + } + + interface Callback { + void onLoadCompleted(List<LauncherActivityInfo> result); + } +} diff --git a/src/com/android/launcher3/lineage/icon/IconPack.java b/src/com/android/launcher3/lineage/icon/IconPack.java new file mode 100644 index 000000000..04b8cd891 --- /dev/null +++ b/src/com/android/launcher3/lineage/icon/IconPack.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2019 Paranoid Android + * Copyright (C) 2020 Shift GmbH + * + * 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.lineage.icon; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.LauncherActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; + +import com.android.launcher3.lineage.icon.providers.IconPackProvider; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public final class IconPack { + /* + * Useful Links: + * https://github.com/teslacoil/Example_NovaTheme + * http://stackoverflow.com/questions/7205415/getting-resources-of-another-application + * http://stackoverflow.com/questions/3890012/how-to-access-string-resource-from-another-application + */ + + private final Context context; + private final String packageName; + + private Map<String, String> iconPackResources; + private List<String> iconBackStrings; + private List<Drawable> iconBackList; + private Drawable iconUpon; + private Drawable iconMask; + private Resources loadedIconPackResource; + private float iconScale; + + public IconPack(Context context, String packageName){ + this.context = context; + this.packageName = packageName; + } + + public void setIcons(Map<String, String> iconPackResources, List<String> iconBackStrings) { + this.iconPackResources = iconPackResources; + this.iconBackStrings = iconBackStrings; + iconBackList = new ArrayList<Drawable>(); + try { + loadedIconPackResource = context.getPackageManager() + .getResourcesForApplication(packageName); + } catch (PackageManager.NameNotFoundException e) { + // must never happen cause it's checked already in the provider + return; + } + + iconMask = getDrawableForName(IconPackProvider.ICON_MASK_TAG); + iconUpon = getDrawableForName(IconPackProvider.ICON_UPON_TAG); + for (int i = 0; i < iconBackStrings.size(); i++) { + final String backIconString = iconBackStrings.get(i); + final Drawable backIcon = getDrawableWithName(backIconString); + if (backIcon != null) { + iconBackList.add(backIcon); + } + } + + final String scale = iconPackResources.get(IconPackProvider.ICON_SCALE_TAG); + if (scale != null) { + try { + iconScale = Float.valueOf(scale); + } catch (NumberFormatException e) { + } + } + } + + public Drawable getIcon(LauncherActivityInfo info, Drawable appIcon, CharSequence appLabel) { + return getIcon(info.getComponentName(), appIcon, appLabel); + } + + public Drawable getIcon(ActivityInfo info, Drawable appIcon, CharSequence appLabel) { + return getIcon(new ComponentName(info.packageName, info.name), appIcon, appLabel); + } + + public Drawable getIcon(ComponentName name, Drawable appIcon, CharSequence appLabel) { + return getDrawable(name.flattenToString(), appIcon, appLabel); + } + + public Drawable getIcon(String packageName, Drawable appIcon, CharSequence appLabel) { + return getDrawable(packageName, appIcon, appLabel); + } + + private Drawable getDrawable(String name, Drawable appIcon, CharSequence appLabel) { + Drawable d = getDrawableForName(name); + if (d == null && appIcon != null) { + d = compose(name, appIcon, appLabel); + } + return d; + } + + private Drawable getIconBackFor(CharSequence tag) { + if (iconBackList == null || iconBackList.size() == 0) { + return null; + } + + if (iconBackList.size() == 1) { + return iconBackList.get(0); + } + + try { + final Drawable back = iconBackList.get( + (tag.hashCode() & 0x7fffffff) % iconBackList.size()); + return back; + } catch (ArrayIndexOutOfBoundsException e) { + return iconBackList.get(0); + } + } + + private int getResourceIdForDrawable(String resource) { + return loadedIconPackResource.getIdentifier(resource, "drawable", packageName); + } + + private Drawable getDrawableForName(String name) { + final String item = iconPackResources.get(name); + if (TextUtils.isEmpty(item)) { + return null; + } + + final int id = getResourceIdForDrawable(item); + return id == 0 ? null : loadedIconPackResource.getDrawable(id); + } + + private Drawable getDrawableWithName(String name) { + final int id = getResourceIdForDrawable(name); + return id == 0 ? null : loadedIconPackResource.getDrawable(id); + } + + private BitmapDrawable getBitmapDrawable(Drawable image) { + if (image instanceof BitmapDrawable) { + return (BitmapDrawable) image; + } + + final Canvas canvas = new Canvas(); + canvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.ANTI_ALIAS_FLAG, + Paint.FILTER_BITMAP_FLAG)); + final Bitmap bmResult = Bitmap.createBitmap(image.getIntrinsicWidth(), + image.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + canvas.setBitmap(bmResult); + image.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + image.draw(canvas); + return new BitmapDrawable(loadedIconPackResource, bmResult); + } + + private Drawable compose(String name, Drawable appIcon, CharSequence appLabel) { + final Canvas canvas = new Canvas(); + canvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.ANTI_ALIAS_FLAG, + Paint.FILTER_BITMAP_FLAG)); + final BitmapDrawable appIconBitmap = getBitmapDrawable(appIcon); + final int width = appIconBitmap.getBitmap().getWidth(); + final int height = appIconBitmap.getBitmap().getHeight(); + float scale = iconScale; + final Drawable iconBack = getIconBackFor(appLabel); + if (iconBack == null && iconMask == null && iconUpon == null){ + scale = 1.0f; + } + + final Bitmap bitmap = Bitmap.createBitmap(width, height, + Bitmap.Config.ARGB_8888); + canvas.setBitmap(bitmap); + final int scaledWidth = (int) (width * scale); + final int scaledHeight = (int) (height * scale); + if (scaledWidth != width || scaledHeight != height) { + final Bitmap scaledBitmap = Bitmap.createScaledBitmap( + appIconBitmap.getBitmap(), scaledWidth, scaledHeight, true); + canvas.drawBitmap(scaledBitmap, (width - scaledWidth) / 2, + (height - scaledHeight) / 2, null); + } else { + canvas.drawBitmap(appIconBitmap.getBitmap(), 0, 0, null); + } + + if (iconMask != null) { + iconMask.setBounds(0, 0, width, height); + BitmapDrawable b = getBitmapDrawable(iconMask); + b.getPaint().setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + b.draw(canvas); + } + if (iconBack != null) { + iconBack.setBounds(0, 0, width, height); + BitmapDrawable b = getBitmapDrawable(iconBack); + b.getPaint().setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); + b.draw(canvas); + } + if (iconUpon != null) { + iconUpon.setBounds(0, 0, width, height); + iconUpon.draw(canvas); + } + + return new BitmapDrawable(loadedIconPackResource, bitmap); + } +} diff --git a/src/com/android/launcher3/lineage/icon/IconPackHeaderPreference.java b/src/com/android/launcher3/lineage/icon/IconPackHeaderPreference.java new file mode 100644 index 000000000..fdb8dc155 --- /dev/null +++ b/src/com/android/launcher3/lineage/icon/IconPackHeaderPreference.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2020 Shift GmbH + * + * 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.lineage.icon; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.view.View; +import android.widget.ImageView; +import android.util.AttributeSet; +import android.util.Log; + +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.CheckBoxPreference; +import androidx.preference.PreferenceViewHolder; + +import com.android.launcher3.R; +import com.android.launcher3.icons.IconProvider; +import com.android.launcher3.lineage.settings.RadioHeaderPreference; + + +public class IconPackHeaderPreference extends RadioHeaderPreference { + private static final String TAG = "IconPackHeaderPreference"; + private static final int PREVIEW_ICON_NUM = 4; + // This value has been selected as an average of usual "device profile-computed" values + private static final int PREVIEW_ICON_DPI = 500; + + private final Context context; + private ImageView[] icons = null; + + public IconPackHeaderPreference(Context context) { + this(context, null); + } + + public IconPackHeaderPreference(Context context, AttributeSet attrs) { + this(context, attrs, TypedArrayUtils.getAttr(context, + androidx.preference.R.attr.preferenceStyle, + android.R.attr.preferenceStyle)); + } + + public IconPackHeaderPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + this.context = context; + + setLayoutResource(R.layout.preference_widget_icons_preview); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + final ImageView[] imageViews = { + (ImageView) holder.findViewById(R.id.pref_icon_a), + (ImageView) holder.findViewById(R.id.pref_icon_b), + (ImageView) holder.findViewById(R.id.pref_icon_c), + (ImageView) holder.findViewById(R.id.pref_icon_d) + }; + this.icons = imageViews; + onRadioElementSelected(null); + } + + @Override + public void onDetached() { + this.icons = null; + super.onDetached(); + } + + @Override + public void onRadioElementSelected(String key) { + if (icons == null) { + return; + } + + final IconProvider iconProvider = new IconProvider(context); + final PackageManager pm = context.getPackageManager(); + final LauncherApps launcherApps = context.getSystemService(LauncherApps.class); + new GetLaunchableInfoTask(pm, launcherApps, PREVIEW_ICON_NUM, (aiList) -> { + for (int i = 0; i < icons.length; i++) { + icons[i].setImageDrawable(iconProvider.getIcon( + aiList.get(i), PREVIEW_ICON_DPI)); + } + }).execute(); + } +} diff --git a/src/com/android/launcher3/lineage/icon/IconPackSettingsActivity.java b/src/com/android/launcher3/lineage/icon/IconPackSettingsActivity.java new file mode 100644 index 000000000..b7bd04afd --- /dev/null +++ b/src/com/android/launcher3/lineage/icon/IconPackSettingsActivity.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2020 Shift GmbH + * + * 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.lineage.icon; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.Fragment; +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.Toast; + +import androidx.preference.Preference; +import androidx.preference.PreferenceFragment; +import androidx.preference.PreferenceFragment.OnPreferenceStartFragmentCallback; +import androidx.preference.PreferenceFragment.OnPreferenceStartScreenCallback; +import androidx.preference.PreferenceScreen; + +import com.android.launcher3.R; +import com.android.launcher3.util.PackageManagerHelper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class IconPackSettingsActivity extends Activity implements + OnPreferenceStartFragmentCallback, OnPreferenceStartScreenCallback { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + + if (savedInstanceState == null) { + final Fragment f = Fragment.instantiate(this, + getString(R.string.icon_pack_settings_class), null); + // Display the fragment as the main content. + getFragmentManager().beginTransaction() + .replace(android.R.id.content, f) + .commit(); + } + } + + private boolean startFragment(String fragment, Bundle args, String key) { + if (getFragmentManager().isStateSaved()) { + // Sometimes onClick can come after onPause because of being posted on the handler. + // Skip starting new fragments in that case. + return false; + } + + final Fragment f = Fragment.instantiate(this, fragment, args); + getFragmentManager() + .beginTransaction() + .replace(android.R.id.content, f) + .addToBackStack(key) + .commit(); + return true; + } + + @Override + public boolean onPreferenceStartFragment(PreferenceFragment preferenceFragment, + Preference pref) { + return startFragment(pref.getFragment(), pref.getExtras(), pref.getKey()); + } + + @Override + public boolean onPreferenceStartScreen(PreferenceFragment caller, PreferenceScreen pref) { + Bundle args = new Bundle(); + args.putString(PreferenceFragment.ARG_PREFERENCE_ROOT, pref.getKey()); + return startFragment(getString(R.string.icon_pack_settings_class), + args, pref.getKey()); + } + + @Override + public boolean onNavigateUp() { + onBackPressed(); + return true; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + final MenuInflater menuInflater = getMenuInflater(); + menuInflater.inflate(R.menu.menu_icon_pack, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == android.R.id.home) { + finish(); + return true; + } else if (id == R.id.menu_icon_pack) { + return openMarket(); + } else { + return super.onOptionsItemSelected(item); + } + } + + private boolean openMarket() { + final String query = getString(R.string.icon_pack_title); + final Intent intent = PackageManagerHelper.getMarketSearchIntent(this, query); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if (intent.resolveActivity(getPackageManager()) == null) { + Toast.makeText(this, R.string.icon_pack_no_market, Toast.LENGTH_LONG) + .show(); + return false; + } + + startActivity(intent); + return true; + } +} diff --git a/src/com/android/launcher3/lineage/icon/IconPackSettingsFragment.java b/src/com/android/launcher3/lineage/icon/IconPackSettingsFragment.java new file mode 100644 index 000000000..f291504a4 --- /dev/null +++ b/src/com/android/launcher3/lineage/icon/IconPackSettingsFragment.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2020 Shift GmbH + * + * 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.lineage.icon; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.preference.Preference; + +import com.android.launcher3.R; +import com.android.launcher3.lineage.settings.RadioPreference; +import com.android.launcher3.lineage.settings.RadioSettingsFragment; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +public final class IconPackSettingsFragment extends RadioSettingsFragment { + private static final IntentFilter PKG_UPDATE_INTENT = new IntentFilter(); + static { + PKG_UPDATE_INTENT.addAction(Intent.ACTION_PACKAGE_INSTALL); + PKG_UPDATE_INTENT.addAction(Intent.ACTION_PACKAGE_ADDED); + PKG_UPDATE_INTENT.addAction(Intent.ACTION_PACKAGE_CHANGED); + PKG_UPDATE_INTENT.addAction(Intent.ACTION_PACKAGE_REMOVED); + PKG_UPDATE_INTENT.addDataScheme("package"); + } + + private IconPackStore iconPackStore = null; + private BroadcastReceiver broadCastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + reloadPreferences(); + } + }; + + @Override + public void onResume() { + super.onResume(); + getActivity().registerReceiver(broadCastReceiver, PKG_UPDATE_INTENT); + } + + @Override + public void onPause() { + super.onPause(); + getActivity().unregisterReceiver(broadCastReceiver); + } + + @Override + protected List<RadioPreference> getRadioPreferences(Context context) { + iconPackStore = new IconPackStore(context); + final String currentIconPack = iconPackStore.getCurrent(); + final List<RadioPreference> prefsList = new ArrayList<>(); + final Set<IconPackInfo> iconPacks = getAvailableIconPacks(context); + + for (final IconPackInfo entry : iconPacks) { + final boolean isCurrent = currentIconPack.equals(entry.pkgName); + final RadioPreference pref = buildPreference(context, + entry.pkgName, entry.label, isCurrent); + prefsList.add(pref); + + if (isCurrent) { + setSelectedPreference(pref); + } + } + + return prefsList; + } + + @Override + public void onSelected(String key) { + if (iconPackStore != null) { + iconPackStore.setCurrent(key); + } + super.onSelected(key); + } + + @Override + protected IconPackHeaderPreference getHeader(Context context) { + return new IconPackHeaderPreference(context); + } + + private Set<IconPackInfo> getAvailableIconPacks(Context context) { + final PackageManager pm = context.getPackageManager(); + final Set<IconPackInfo> availablePacks = new LinkedHashSet<>(); + final List<ResolveInfo> eligiblePacks = new ArrayList<>(); + eligiblePacks.addAll(pm.queryIntentActivities( + new Intent("com.novalauncher.THEME"), 0)); + eligiblePacks.addAll(pm.queryIntentActivities( + new Intent("org.adw.launcher.icons.ACTION_PICK_ICON"), 0)); + + // Add default + final String defaultLabel = context.getString(R.string.icon_pack_default_label); + availablePacks.add(new IconPackInfo(IconPackStore.SYSTEM_ICON_PACK, defaultLabel)); + // Add user-installed packs + for (final ResolveInfo r : eligiblePacks) { + availablePacks.add(new IconPackInfo( + r.activityInfo.packageName, (String) r.loadLabel(pm))); + } + return availablePacks; + } + + private RadioPreference buildPreference(Context context, String pkgName, + String label, boolean isChecked) { + final RadioPreference pref = new RadioPreference(context); + pref.setKey(pkgName); + pref.setTitle(label); + pref.setPersistent(false); + pref.setChecked(isChecked); + return pref; + } + + private static class IconPackInfo { + final String pkgName; + final String label; + + IconPackInfo(String pkgName, String label) { + this.pkgName = pkgName; + this.label = label; + } + + @Override + public int hashCode() { + return pkgName.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == null) return false; + if (!(other instanceof IconPackInfo)) return false; + return pkgName.equals(((IconPackInfo) other).pkgName); + } + } +} diff --git a/src/com/android/launcher3/lineage/icon/IconPackStore.java b/src/com/android/launcher3/lineage/icon/IconPackStore.java new file mode 100644 index 000000000..fd25fb703 --- /dev/null +++ b/src/com/android/launcher3/lineage/icon/IconPackStore.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 Shift GmbH + * + * 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.lineage.icon; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +import com.android.launcher3.R; + +public final class IconPackStore { + public static final String SYSTEM_ICON_PACK = "android"; + public static final String KEY_ICON_PACK = "pref_iconPackPackage"; + + private Context context; + private SharedPreferences prefs; + + public IconPackStore(Context context) { + this.context = context; + this.prefs = context.getSharedPreferences( + "com.android.launcher3.prefs", Context.MODE_PRIVATE); + } + + public String getCurrent() { + return prefs.getString(KEY_ICON_PACK, getDefaultIconPack()); + } + + public void setCurrent(String pkgName) { + prefs.edit() + .putString(KEY_ICON_PACK, pkgName) + .apply(); + } + + public boolean isUsingSystemIcons() { + return SYSTEM_ICON_PACK.equals(getCurrent()); + } + + public String getCurrentLabel(String defaultLabel) { + final String pkgName = getCurrent(); + if (SYSTEM_ICON_PACK.equals(pkgName)) { + return defaultLabel; + } + + final PackageManager pm = context.getPackageManager(); + try { + final ApplicationInfo ai = pm.getApplicationInfo(pkgName, 0); + return (String) pm.getApplicationLabel(ai); + } catch (PackageManager.NameNotFoundException e) { + return defaultLabel; + } + } + + private String getDefaultIconPack() { + return context.getString(R.string.icon_pack_default_pkg); + } +} diff --git a/src/com/android/launcher3/lineage/icon/LineageIconFactory.java b/src/com/android/launcher3/lineage/icon/LineageIconFactory.java new file mode 100644 index 000000000..3e30c4360 --- /dev/null +++ b/src/com/android/launcher3/lineage/icon/LineageIconFactory.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2021 The LineageOS Project + * Copyright (C) 2021 Shift GmbH + * + * 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.lineage.icon; + + +import static android.graphics.Paint.DITHER_FLAG; +import static android.graphics.Paint.FILTER_BITMAP_FLAG; + +import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Process; +import android.os.UserHandle; + +import androidx.annotation.NonNull; + +import com.android.launcher3.icons.BaseIconFactory; + +public class LineageIconFactory /* extends BaseIconFactory */{ +/* + private final Context mContext; + private final PackageManager mPm; + private final IconPackStore mIconPackStore; + + public LineageIconFactory(Context context) { + mContext = context; + mPm = context.getPackageManager(); + mIconPackStore = new IconPackStore(context); + } + + @Override + public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon, UserHandle user, + boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale) { + if (scale == null) { + scale = new float[1]; + } + } + + @Override + public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) { + RectF iconBounds = new RectF(); + float[] scale = new float[1]; + icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, iconBounds, scale); + return createIconBitmap(icon, + Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds))); + } + + private Drawable normalizeAndWrapToAdaptiveIcon(@NonNull Drawable icon, + boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale) { + if (icon == null) { + return null; + } + float scale = 1f; + + final boolean defaultIcons = mIconPackStore.isUsingSystemIcons(); + if (shrinkNonAdaptiveIcons && ATLEAST_OREO && defaultIcons) { + if (mWrapperIcon == null) { + mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper) + .mutate(); + } + AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon; + dr.setBounds(0, 0, 1, 1); + boolean[] outShape = new boolean[1]; + scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape); + if (!(icon instanceof AdaptiveIconDrawable) && !outShape[0]) { + FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground()); + fsd.setDrawable(icon); + fsd.setScale(scale); + icon = dr; + scale = getNormalizer().getScale(icon, outIconBounds, null, null); + + ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor); + } + } else { + scale = getNormalizer().getScale(icon, outIconBounds, null, null); + } + + outScale[0] = scale; + return icon; + } +*/ +} diff --git a/src/com/android/launcher3/lineage/icon/providers/IconPackProvider.java b/src/com/android/launcher3/lineage/icon/providers/IconPackProvider.java new file mode 100644 index 000000000..dfedf0607 --- /dev/null +++ b/src/com/android/launcher3/lineage/icon/providers/IconPackProvider.java @@ -0,0 +1,172 @@ +/* + * 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.lineage.icon.providers; + +import android.content.Context; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.launcher3.Utilities; +import com.android.launcher3.lineage.icon.IconPack; +import com.android.launcher3.lineage.icon.IconPackStore; + +import org.xmlpull.v1.XmlPullParser; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class IconPackProvider { + private static final String TAG = "IconPackProvider"; + + private static Map<String, IconPack> iconPacks = new ArrayMap<>(); + + public static final String ICON_MASK_TAG = "iconmask"; + public static final String ICON_BACK_TAG = "iconback"; + public static final String ICON_UPON_TAG = "iconupon"; + public static final String ICON_SCALE_TAG = "scale"; + + private IconPackProvider() { + } + + public static IconPack getIconPack(String packageName){ + return iconPacks.get(packageName); + } + + public static IconPack loadAndGetIconPack(Context context) { + final String packageName = new IconPackStore(context).getCurrent(); + if (IconPackStore.SYSTEM_ICON_PACK.equals(packageName)){ + return null; + } + + if (!iconPacks.containsKey(packageName)){ + loadIconPack(context, packageName); + } + return getIconPack(packageName); + } + + public static void loadIconPack(Context context, String packageName) { + if (IconPackStore.SYSTEM_ICON_PACK.equals(packageName)){ + iconPacks.put("", null); + } + + try { + final XmlPullParser appFilter = getAppFilter(context, packageName); + if (appFilter != null) { + final IconPack pack = new IconPack(context, packageName); + parseAppFilter(packageName, appFilter, pack); + iconPacks.put(packageName, pack); + } + } catch (Exception e) { + Log.e(TAG, "Invalid IconPack", e); + return; + } + } + + private static void parseAppFilter(String packageName, XmlPullParser parser, + IconPack pack) throws Exception { + final Map<String, String> iconPackResources = new HashMap<>(); + final List<String> iconBackStrings = new ArrayList<>(); + + while (parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + + final String name = parser.getName(); + if (name.equals("item")) { + String component = parser.getAttributeValue(null, "component"); + final String drawable = parser.getAttributeValue(null, "drawable"); + // Validate component/drawable exist + if (TextUtils.isEmpty(component) || TextUtils.isEmpty(drawable)) { + continue; + } + // Validate format/length of component + if (!component.startsWith("ComponentInfo{") || !component.endsWith("}") || + component.length() < 16) { + continue; + } + // Sanitize stored value + component = component.substring(14, component.length() - 1); + if (!component.contains("/")) { + // Package icon reference + iconPackResources.put(component, drawable); + } else { + final ComponentName componentName = ComponentName.unflattenFromString( + component); + if (componentName != null) { + iconPackResources.put(componentName.getPackageName(), drawable); + iconPackResources.put(component, drawable); + } + } + continue; + } + + if (name.equalsIgnoreCase(ICON_BACK_TAG)) { + final String icon = parser.getAttributeValue(null, "img"); + if (icon == null) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + iconBackStrings.add(parser.getAttributeValue(i)); + } + } + continue; + } + + if (name.equalsIgnoreCase(ICON_MASK_TAG) || name.equalsIgnoreCase(ICON_UPON_TAG)) { + String icon = parser.getAttributeValue(null, "img"); + if (icon == null) { + if (parser.getAttributeCount() > 0) { + icon = parser.getAttributeValue(0); + } + } + iconPackResources.put(parser.getName().toLowerCase(), icon); + continue; + } + + if (name.equalsIgnoreCase(ICON_SCALE_TAG)) { + String factor = parser.getAttributeValue(null, "factor"); + if (factor == null) { + if (parser.getAttributeCount() > 0) { + factor = parser.getAttributeValue(0); + } + } + iconPackResources.put(parser.getName().toLowerCase(), factor); + continue; + } + } + + pack.setIcons(iconPackResources, iconBackStrings); + } + + private static XmlPullParser getAppFilter(Context context, String packageName) { + try { + final Resources res = context.getPackageManager() + .getResourcesForApplication(packageName); + final int resourceId = res.getIdentifier("appfilter", "xml", packageName); + if (0 != resourceId) { + return context.getPackageManager().getXml(packageName, resourceId, null); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Failed to get AppFilter", e); + } + return null; + } +} diff --git a/src/com/android/launcher3/lineage/settings/RadioHeaderPreference.java b/src/com/android/launcher3/lineage/settings/RadioHeaderPreference.java new file mode 100644 index 000000000..d72abfcb1 --- /dev/null +++ b/src/com/android/launcher3/lineage/settings/RadioHeaderPreference.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020 Shift GmbH + * + * 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.lineage.settings; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.Preference; + +import com.android.launcher3.R; + +public abstract class RadioHeaderPreference extends Preference { + + public RadioHeaderPreference(Context context) { + this(context, null); + } + + public RadioHeaderPreference(Context context, AttributeSet attrs) { + this(context, attrs, TypedArrayUtils.getAttr(context, + androidx.preference.R.attr.preferenceStyle, + android.R.attr.preferenceStyle)); + } + + public RadioHeaderPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setIconSpaceReserved(false); + } + + public abstract void onRadioElementSelected(String key); +} diff --git a/src/com/android/launcher3/lineage/settings/RadioPreference.java b/src/com/android/launcher3/lineage/settings/RadioPreference.java new file mode 100644 index 000000000..bbedbf572 --- /dev/null +++ b/src/com/android/launcher3/lineage/settings/RadioPreference.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020 Shift GmbH + * + * 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.lineage.settings; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.CheckBoxPreference; + +import com.android.launcher3.R; + +public class RadioPreference extends CheckBoxPreference { + + public RadioPreference(Context context) { + this(context, null); + } + + public RadioPreference(Context context, AttributeSet attrs) { + this(context, attrs, TypedArrayUtils.getAttr(context, + androidx.preference.R.attr.preferenceStyle, + android.R.attr.preferenceStyle)); + } + + public RadioPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setWidgetLayoutResource(R.layout.preference_widget_radiobutton); + setLayoutResource(R.layout.preference_radio); + setIconSpaceReserved(false); + } +} diff --git a/src/com/android/launcher3/lineage/settings/RadioSettingsFragment.java b/src/com/android/launcher3/lineage/settings/RadioSettingsFragment.java new file mode 100644 index 000000000..293558031 --- /dev/null +++ b/src/com/android/launcher3/lineage/settings/RadioSettingsFragment.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2020 Shift GmbH + * + * 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.lineage.settings; + +import android.content.Context; +import android.os.Bundle; + +import androidx.preference.Preference; +import androidx.preference.PreferenceFragment; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; + +import java.util.List; + +public abstract class RadioSettingsFragment extends PreferenceFragment implements + Preference.OnPreferenceClickListener { + private RadioPreference selectedPreference = null; + private RadioHeaderPreference headerPref = null; + + protected abstract List<RadioPreference> getRadioPreferences(Context context); + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + final PreferenceManager prefManager = getPreferenceManager(); + final Context context = prefManager.getContext(); + final PreferenceScreen screen = prefManager.createPreferenceScreen(context); + + headerPref = getHeader(context); + if (headerPref != null) { + screen.addPreference(headerPref); + } + + loadRadioPreferences(context, screen, null); + setPreferenceScreen(screen); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (preference instanceof RadioPreference) { + onSelected(preference.getKey()); + + if (selectedPreference != null) { + selectedPreference.setChecked(false); + } + selectedPreference = (RadioPreference) preference; + selectedPreference.setChecked(true); + return true; + } else { + return false; + } + } + + @Override + public void onDestroyView() { + selectedPreference = null; + headerPref = null; + super.onDestroyView(); + } + + protected RadioHeaderPreference getHeader(Context context) { + return null; + } + + protected final void setSelectedPreference(RadioPreference preference) { + selectedPreference = preference; + } + + protected void onSelected(String key) { + if (headerPref != null) { + headerPref.onRadioElementSelected(key); + } + } + + protected void reloadPreferences() { + final PreferenceScreen screen = getPreferenceScreen(); + if (screen == null) { + return; + } + + // Save current key for later + final String currentKey = selectedPreference == null ? + null : selectedPreference.getKey(); + selectedPreference = null; + + // Reload header contents + if (headerPref != null) { + headerPref.onRadioElementSelected(currentKey); + } + + // Remove radio preferences (backwards so we don't mess up indices) + final int numPreferences = screen.getPreferenceCount(); + for (int i = numPreferences - 1; i >= 0; i--) { + final Preference p = screen.getPreference(i); + if (p instanceof RadioPreference) { + screen.removePreference(p); + } + } + + // Add radio preferences + final PreferenceManager prefManager = getPreferenceManager(); + final Context context = prefManager.getContext(); + loadRadioPreferences(context, screen, currentKey); + } + + private void loadRadioPreferences(Context context, PreferenceScreen screen, + String currentKey) { + boolean hasSetNewCurrent = false; + + final List<RadioPreference> prefs = getRadioPreferences(context); + for (final RadioPreference p : prefs) { + if (currentKey != null && currentKey.equals(p.getKey())) { + p.setChecked(true); + selectedPreference = p; + hasSetNewCurrent = true; + } + p.setOnPreferenceClickListener(this); + screen.addPreference(p); + } + + if (!hasSetNewCurrent && currentKey != null && !prefs.isEmpty()) { + // Old "current" preference was removed, fallback to + // the first candidate + selectedPreference = prefs.get(0); + selectedPreference.setChecked(true); + onPreferenceClick(selectedPreference); + } + } +} diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java index ee0efbed3..2091ac69c 100644 --- a/src/com/android/launcher3/settings/SettingsActivity.java +++ b/src/com/android/launcher3/settings/SettingsActivity.java @@ -23,6 +23,9 @@ import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERE import static com.android.launcher3.states.RotationHelper.getAllowRotationDefaultValue; import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver; +import android.app.Activity; +import android.app.ActivityManager; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -47,6 +50,8 @@ import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.lineage.LineageUtils; +import com.android.launcher3.lineage.icon.IconPackStore; +import com.android.launcher3.lineage.icon.IconPackSettingsActivity; import com.android.launcher3.lineage.trust.TrustAppsActivity; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.util.SecureSettingsObserver; @@ -71,6 +76,7 @@ public class SettingsActivity extends FragmentActivity public static final String SAVE_HIGHLIGHTED_KEY = "android:preference_highlighted"; public static final String KEY_TRUST_APPS = "pref_trust_apps"; + public static final String KEY_ICON_PACK = "pref_icon_pack"; private static final String KEY_MINUS_ONE = "pref_enable_minus_one"; private static final String SEARCH_PACKAGE = "com.google.android.googlequicksearchbox"; @@ -132,7 +138,8 @@ public class SettingsActivity extends FragmentActivity /** * This fragment shows the launcher preferences. */ - public static class LauncherSettingsFragment extends PreferenceFragmentCompat { + public static class LauncherSettingsFragment extends PreferenceFragmentCompat implements + SharedPreferences.OnSharedPreferenceChangeListener { private SecureSettingsObserver mNotificationDotsObserver; @@ -154,6 +161,20 @@ public class SettingsActivity extends FragmentActivity getPreferenceManager().setSharedPreferencesName(LauncherFiles.SHARED_PREFERENCES_KEY); setPreferencesFromResource(R.xml.launcher_preferences, rootKey); + updatePreferences(); + + Utilities.getPrefs(getContext()) + .registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onDestroyView () { + Utilities.getPrefs(getContext()) + .unregisterOnSharedPreferenceChangeListener(this); + super.onDestroyView(); + } + + private void updatePreferences() { PreferenceScreen screen = getPreferenceScreen(); for (int i = screen.getPreferenceCount() - 1; i >= 0; i--) { Preference preference = screen.getPreference(i); @@ -169,6 +190,15 @@ public class SettingsActivity extends FragmentActivity outState.putBoolean(SAVE_HIGHLIGHTED_KEY, mPreferenceHighlighted); } + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + switch (key) { + case IconPackStore.KEY_ICON_PACK: + updatePreferences(); + break; + } + } + protected String getParentKeyForPref(String key) { return null; } @@ -230,6 +260,9 @@ public class SettingsActivity extends FragmentActivity return true; }); return true; + case KEY_ICON_PACK: + setupIconPackPreference(preference); + return true; } return true; @@ -283,5 +316,16 @@ public class SettingsActivity extends FragmentActivity } super.onDestroy(); } + + private void setupIconPackPreference(Preference preference) { + final Context context = getContext(); + final String defaultLabel = context.getString(R.string.icon_pack_default_label); + final String pkgLabel = new IconPackStore(context).getCurrentLabel(defaultLabel); + preference.setSummary(pkgLabel); + preference.setOnPreferenceClickListener(p -> { + startActivity(new Intent(getActivity(), IconPackSettingsActivity.class)); + return true; + }); + } } } diff --git a/src/com/android/launcher3/util/ConfigMonitor.java b/src/com/android/launcher3/util/ConfigMonitor.java index 0f8152057..c42cef355 100644 --- a/src/com/android/launcher3/util/ConfigMonitor.java +++ b/src/com/android/launcher3/util/ConfigMonitor.java @@ -22,10 +22,14 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.res.Configuration; import android.graphics.Point; import android.util.Log; +import com.android.launcher3.Utilities; +import com.android.launcher3.lineage.icon.IconPackStore; + import java.util.function.Consumer; /** @@ -33,7 +37,8 @@ import java.util.function.Consumer; * notifies the callback in case changes which affect the device profile occur. */ public class ConfigMonitor extends BroadcastReceiver implements - DefaultDisplay.DisplayInfoChangeListener { + DefaultDisplay.DisplayInfoChangeListener, + SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = "ConfigMonitor"; @@ -70,6 +75,7 @@ public class ConfigMonitor extends BroadcastReceiver implements // Listen for configuration change mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)); + Utilities.getPrefs(mContext).registerOnSharedPreferenceChangeListener(this); } @Override @@ -102,6 +108,13 @@ public class ConfigMonitor extends BroadcastReceiver implements } } + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (IconPackStore.KEY_ICON_PACK.equals(key)) { + notifyChange(); + } + } + private synchronized void notifyChange() { if (mCallback != null) { Consumer<Context> callback = mCallback; @@ -115,6 +128,7 @@ public class ConfigMonitor extends BroadcastReceiver implements mContext.unregisterReceiver(this); DefaultDisplay display = DefaultDisplay.INSTANCE.get(mContext); display.removeChangeListener(this); + Utilities.getPrefs(mContext).unregisterOnSharedPreferenceChangeListener(this); } catch (Exception e) { Log.e(TAG, "Failed to unregister config monitor", e); } |