summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk15
-rw-r--r--AndroidManifest.xml57
-rw-r--r--MODULE_LICENSE_APACHE2 (renamed from EMPTY)0
-rw-r--r--assets/default_holo_theme/holo_boot_anim.jpgbin0 -> 21211 bytes
-rw-r--r--assets/default_holo_theme/holo_homescreen.pngbin0 -> 1144392 bytes
-rw-r--r--assets/default_holo_theme/holo_lockscreen.pngbin0 -> 1065767 bytes
-rw-r--r--assets/default_holo_theme/style.jpgbin0 -> 110358 bytes
-rw-r--r--res/drawable-hdpi/ic_app_themes.pngbin0 -> 1843 bytes
-rwxr-xr-xres/drawable-hdpi/ic_app_themes_bw.pngbin0 -> 226 bytes
-rw-r--r--res/drawable-mdpi/ic_app_themes.pngbin0 -> 1653 bytes
-rwxr-xr-xres/drawable-mdpi/ic_app_themes_bw.pngbin0 -> 205 bytes
-rw-r--r--res/drawable-xhdpi/ic_app_themes.pngbin0 -> 2217 bytes
-rwxr-xr-xres/drawable-xhdpi/ic_app_themes_bw.pngbin0 -> 274 bytes
-rw-r--r--res/drawable-xhdpi/ic_drawer.pngbin0 -> 1038 bytes
-rw-r--r--res/drawable-xxhdpi/ic_app_themes.pngbin0 -> 1096 bytes
-rwxr-xr-xres/drawable-xxhdpi/ic_app_themes_bw.pngbin0 -> 358 bytes
-rw-r--r--res/drawable-xxhdpi/ic_notifiy.pngbin0 -> 564 bytes
-rw-r--r--res/drawable/apply_button_bg.xml7
-rw-r--r--res/drawable/apply_button_text.xml7
-rw-r--r--res/drawable/progress.xml6
-rw-r--r--res/drawable/progress_bg.xml7
-rw-r--r--res/drawable/progress_bg2.xml7
-rw-r--r--res/layout/activity_main.xml21
-rw-r--r--res/layout/audible_preview_item.xml39
-rw-r--r--res/layout/audibles_preview.xml28
-rw-r--r--res/layout/font_preview_item.xml57
-rw-r--r--res/layout/fragment_boot_animation_preview.xml41
-rw-r--r--res/layout/fragment_chooser_browse.xml38
-rw-r--r--res/layout/fragment_chooser_component_browse.xml20
-rw-r--r--res/layout/fragment_chooser_theme_pager_item.xml146
-rw-r--r--res/layout/image_preview_item.xml34
-rw-r--r--res/layout/item_chooser_browse_font.xml86
-rw-r--r--res/layout/item_store_browse.xml74
-rw-r--r--res/values-land/dimens.xml19
-rw-r--r--res/values/attrs.xml40
-rw-r--r--res/values/colors.xml29
-rw-r--r--res/values/dimens.xml22
-rw-r--r--res/values/strings.xml51
-rw-r--r--res/values/styles.xml111
-rw-r--r--src/com/sothree/slidinguppanel/SlidingupPanelLayout.java1282
-rw-r--r--src/org/cyanogenmod/theme/chooser/AppReceiver.java58
-rw-r--r--src/org/cyanogenmod/theme/chooser/AudiblePreviewFragment.java298
-rw-r--r--src/org/cyanogenmod/theme/chooser/BootAniPreviewFragment.java245
-rw-r--r--src/org/cyanogenmod/theme/chooser/ChooserActivity.java70
-rw-r--r--src/org/cyanogenmod/theme/chooser/ChooserBrowseFragment.java439
-rw-r--r--src/org/cyanogenmod/theme/chooser/ChooserDetailFragment.java516
-rw-r--r--src/org/cyanogenmod/theme/chooser/FontPreviewFragment.java84
-rw-r--r--src/org/cyanogenmod/theme/chooser/NotificationHijackingService.java87
-rw-r--r--src/org/cyanogenmod/theme/chooser/WallpaperAndIconPreviewFragment.java286
-rw-r--r--src/org/cyanogenmod/theme/util/BootAnimationHelper.java257
-rw-r--r--src/org/cyanogenmod/theme/util/CustomTypeFaceSpan.java55
-rw-r--r--src/org/cyanogenmod/theme/util/FittedTextView.java71
-rw-r--r--src/org/cyanogenmod/theme/util/FontConfigParser.java173
-rw-r--r--src/org/cyanogenmod/theme/util/IconPreviewHelper.java153
-rw-r--r--src/org/cyanogenmod/theme/util/NotificationHelper.java75
-rw-r--r--src/org/cyanogenmod/theme/util/ThemedTypefaceHelper.java126
-rw-r--r--src/org/cyanogenmod/theme/util/Utils.java238
-rw-r--r--src/org/cyanogenmod/theme/widget/PartAnimationDrawable.java40
58 files changed, 5515 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..bedc48d
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,15 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PACKAGE_NAME := ThemeChooser
+LOCAL_CERTIFICATE := platform
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-v4 \
+
+include $(BUILD_PACKAGE)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..5605678
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.cyanogenmod.theme.chooser"
+ android:sharedUserId="org.cyanogenmod.themes"
+ android:versionCode="7"
+ android:versionName="1.0" >
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_THEME_MANAGER" />
+
+ <!-- The following permissions are used to hijack Google Play notifications
+ when a theme is installed -->
+ <uses-permission android:name="android.permission.CANCEL_NOTIFICATIONS" />
+ <uses-permission android:name="android.permission.ACCESS_NOTIFICATIONS" />
+ <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
+ <uses-sdk
+ android:minSdkVersion="19"
+ android:targetSdkVersion="19" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_app_themes"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+ <activity
+ android:name="org.cyanogenmod.theme.chooser.ChooserActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="cyanogenmod.intent.category.APP_THEMES" />
+ </intent-filter>
+ <intent-filter >
+ <action android:name="android.intent.action.SET_WALLPAPER" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <receiver android:name="org.cyanogenmod.theme.chooser.AppReceiver" >
+ <intent-filter>
+ <action android:name="android.intent.action.PACKAGE_ADDED" />
+ <action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
+ <data android:scheme="package" />
+ </intent-filter>
+ </receiver>
+
+ <service android:name="org.cyanogenmod.theme.chooser.NotificationHijackingService"
+ android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+ <intent-filter>
+ <action android:name="android.service.notification.NotificationListenerService" />
+ </intent-filter>
+ </service>
+ </application>
+
+</manifest> \ No newline at end of file
diff --git a/EMPTY b/MODULE_LICENSE_APACHE2
index e69de29..e69de29 100644
--- a/EMPTY
+++ b/MODULE_LICENSE_APACHE2
diff --git a/assets/default_holo_theme/holo_boot_anim.jpg b/assets/default_holo_theme/holo_boot_anim.jpg
new file mode 100644
index 0000000..b8ebdfe
--- /dev/null
+++ b/assets/default_holo_theme/holo_boot_anim.jpg
Binary files differ
diff --git a/assets/default_holo_theme/holo_homescreen.png b/assets/default_holo_theme/holo_homescreen.png
new file mode 100644
index 0000000..41624d6
--- /dev/null
+++ b/assets/default_holo_theme/holo_homescreen.png
Binary files differ
diff --git a/assets/default_holo_theme/holo_lockscreen.png b/assets/default_holo_theme/holo_lockscreen.png
new file mode 100644
index 0000000..d48e9c5
--- /dev/null
+++ b/assets/default_holo_theme/holo_lockscreen.png
Binary files differ
diff --git a/assets/default_holo_theme/style.jpg b/assets/default_holo_theme/style.jpg
new file mode 100644
index 0000000..abd397d
--- /dev/null
+++ b/assets/default_holo_theme/style.jpg
Binary files differ
diff --git a/res/drawable-hdpi/ic_app_themes.png b/res/drawable-hdpi/ic_app_themes.png
new file mode 100644
index 0000000..b2bdd60
--- /dev/null
+++ b/res/drawable-hdpi/ic_app_themes.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_app_themes_bw.png b/res/drawable-hdpi/ic_app_themes_bw.png
new file mode 100755
index 0000000..f8488d0
--- /dev/null
+++ b/res/drawable-hdpi/ic_app_themes_bw.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_app_themes.png b/res/drawable-mdpi/ic_app_themes.png
new file mode 100644
index 0000000..9266504
--- /dev/null
+++ b/res/drawable-mdpi/ic_app_themes.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_app_themes_bw.png b/res/drawable-mdpi/ic_app_themes_bw.png
new file mode 100755
index 0000000..bb48219
--- /dev/null
+++ b/res/drawable-mdpi/ic_app_themes_bw.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_app_themes.png b/res/drawable-xhdpi/ic_app_themes.png
new file mode 100644
index 0000000..85f85ee
--- /dev/null
+++ b/res/drawable-xhdpi/ic_app_themes.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_app_themes_bw.png b/res/drawable-xhdpi/ic_app_themes_bw.png
new file mode 100755
index 0000000..3a65585
--- /dev/null
+++ b/res/drawable-xhdpi/ic_app_themes_bw.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_drawer.png b/res/drawable-xhdpi/ic_drawer.png
new file mode 100644
index 0000000..bcf49dd
--- /dev/null
+++ b/res/drawable-xhdpi/ic_drawer.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_app_themes.png b/res/drawable-xxhdpi/ic_app_themes.png
new file mode 100644
index 0000000..fbbf03b
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_app_themes.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_app_themes_bw.png b/res/drawable-xxhdpi/ic_app_themes_bw.png
new file mode 100755
index 0000000..a1c5c4b
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_app_themes_bw.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_notifiy.png b/res/drawable-xxhdpi/ic_notifiy.png
new file mode 100644
index 0000000..ff86c5c
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_notifiy.png
Binary files differ
diff --git a/res/drawable/apply_button_bg.xml b/res/drawable/apply_button_bg.xml
new file mode 100644
index 0000000..b598916
--- /dev/null
+++ b/res/drawable/apply_button_bg.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false" android:drawable="@color/author_grey" />
+ <item android:state_pressed="true" android:drawable="@color/apply_button_pressed_color" />
+ <item android:drawable="@drawable/progress" />
+</selector> \ No newline at end of file
diff --git a/res/drawable/apply_button_text.xml b/res/drawable/apply_button_text.xml
new file mode 100644
index 0000000..1eb1088
--- /dev/null
+++ b/res/drawable/apply_button_text.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:color="@color/apply_button_text_color_disabled" />
+ <item android:color="@color/apply_button_text_color_enabled" />
+</selector> \ No newline at end of file
diff --git a/res/drawable/progress.xml b/res/drawable/progress.xml
new file mode 100644
index 0000000..8e65bd5
--- /dev/null
+++ b/res/drawable/progress.xml
@@ -0,0 +1,6 @@
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@android:id/background" android:drawable="@drawable/progress_bg"/>
+ <item android:id="@android:id/progress">
+ <clip android:drawable="@drawable/progress_bg2"/>
+ </item>
+</layer-list>
diff --git a/res/drawable/progress_bg.xml b/res/drawable/progress_bg.xml
new file mode 100644
index 0000000..6b0a128
--- /dev/null
+++ b/res/drawable/progress_bg.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid
+ android:color="@color/apply_button_default_color" />
+</shape> \ No newline at end of file
diff --git a/res/drawable/progress_bg2.xml b/res/drawable/progress_bg2.xml
new file mode 100644
index 0000000..24725ed
--- /dev/null
+++ b/res/drawable/progress_bg2.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid
+ android:color="@color/apply_button_progress_color" />
+</shape> \ No newline at end of file
diff --git a/res/layout/activity_main.xml b/res/layout/activity_main.xml
new file mode 100644
index 0000000..ed80e0d
--- /dev/null
+++ b/res/layout/activity_main.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
diff --git a/res/layout/audible_preview_item.xml b/res/layout/audible_preview_item.xml
new file mode 100644
index 0000000..a977170
--- /dev/null
+++ b/res/layout/audible_preview_item.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/audible_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center"
+ android:layout_marginStart="16dp"
+ style="@android:style/TextAppearance.Large"/>
+
+ <ImageView
+ android:id="@+id/btn_play_pause"
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:layout_marginEnd="16dp"
+ android:padding="5dp"
+ android:scaleType="fitCenter"
+ android:src="@android:drawable/ic_media_play" />
+</LinearLayout>
diff --git a/res/layout/audibles_preview.xml b/res/layout/audibles_preview.xml
new file mode 100644
index 0000000..6a9414a
--- /dev/null
+++ b/res/layout/audibles_preview.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ android:id="@+id/audibles_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical" />
+
+</FrameLayout>
diff --git a/res/layout/font_preview_item.xml b/res/layout/font_preview_item.xml
new file mode 100644
index 0000000..f8b4ab5
--- /dev/null
+++ b/res/layout/font_preview_item.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="@color/offwhite"
+ android:paddingLeft="30dp"
+ android:paddingRight="40dp"
+ android:paddingTop="40dp">
+ <org.cyanogenmod.theme.util.FittedTextView
+ android:id="@+id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:singleLine="true"
+ android:ellipsize="none"
+ android:text="@string/font_preview_letters" />
+ <org.cyanogenmod.theme.util.FittedTextView
+ android:id="@+id/text2"
+ android:singleLine="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:ellipsize="none"
+ android:text="@string/font_preview_letters" />
+ <org.cyanogenmod.theme.util.FittedTextView
+ android:id="@+id/text3"
+ android:singleLine="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:ellipsize="none"
+ android:text="@string/font_preview_letters" />
+ <org.cyanogenmod.theme.util.FittedTextView
+ android:id="@+id/text4"
+ android:singleLine="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:ellipsize="none"
+ android:text="@string/font_preview_letters" />
+</LinearLayout>
diff --git a/res/layout/fragment_boot_animation_preview.xml b/res/layout/fragment_boot_animation_preview.xml
new file mode 100644
index 0000000..abd3c99
--- /dev/null
+++ b/res/layout/fragment_boot_animation_preview.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical" android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/animated_preview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <TextView
+ android:id="@+id/no_preview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/no_boot_animation_preview"
+ android:visibility="invisible" />
+
+ <ProgressBar
+ android:id="@+id/loading_progress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/fragment_chooser_browse.xml b/res/layout/fragment_chooser_browse.xml
new file mode 100644
index 0000000..720825a
--- /dev/null
+++ b/res/layout/fragment_chooser_browse.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".MainActivity$PlaceholderFragment" >
+
+ <ListView
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:divider="@null" />
+
+ <ProgressBar
+ android:id="@+id/loadingAnim"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:visibility="gone" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/fragment_chooser_component_browse.xml b/res/layout/fragment_chooser_component_browse.xml
new file mode 100644
index 0000000..6d6297f
--- /dev/null
+++ b/res/layout/fragment_chooser_component_browse.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/res/layout/fragment_chooser_theme_pager_item.xml b/res/layout/fragment_chooser_theme_pager_item.xml
new file mode 100644
index 0000000..1943cec
--- /dev/null
+++ b/res/layout/fragment_chooser_theme_pager_item.xml
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:themes="http://schemas.android.com/apk/res/org.cyanogenmod.theme.chooser"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <com.sothree.slidinguppanel.SlidingupPanelLayout
+ android:id="@+id/sliding_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_above="@+id/apply"
+ android:layout_gravity="bottom"
+ android:clickable="false"
+ themes:parallaxDistance="@dimen/sliding_up_panel_parallax_distance"
+ themes:anchorPoint="0.5" >
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/detailedview_pager_background"
+ android:paddingLeft="24dp"
+ android:paddingRight="10dp" >
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="220sp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:paddingTop="6dp"
+ android:textSize="28sp" />
+
+ <TextView
+ android:id="@+id/author"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@id/title"
+ android:layout_marginTop="-2dp"
+ android:text="@string/unknown_author"
+ android:textSize="12sp" />
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/author" >
+ <LinearLayout
+ android:id="@+id/details"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="6dp" >
+
+ <CheckBox
+ android:id="@+id/chk_overlays"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/style" />
+
+ <CheckBox
+ android:id="@+id/chk_wallpaper"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/wallpapers" />
+
+ <CheckBox
+ android:id="@+id/chk_lockscreen"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/lock_screen" />
+
+ <CheckBox
+ android:id="@+id/chk_fonts"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/fonts" />
+
+ <CheckBox
+ android:id="@+id/chk_icons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/icons" />
+
+ <CheckBox
+ android:id="@+id/chk_boot_anims"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/boot_anims" />
+
+ <CheckBox
+ android:id="@+id/chk_ringtones"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/ringtones" />
+
+ <CheckBox
+ android:id="@+id/chk_notifications"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/notifications" />
+
+ <CheckBox
+ android:id="@+id/chk_alarms"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/alarms" />
+ </LinearLayout>
+ </ScrollView>
+ </RelativeLayout>
+ </com.sothree.slidinguppanel.SlidingupPanelLayout>
+
+ <Button
+ android:id="@+id/apply"
+ android:layout_width="match_parent"
+ android:layout_height="60dp"
+ android:layout_alignParentBottom="true"
+ android:enabled="false"
+ android:background="@drawable/apply_button_bg"
+ android:text="@string/apply"
+ android:textColor="@drawable/apply_button_text"
+ android:textStyle="bold" />
+
+</RelativeLayout>
diff --git a/res/layout/image_preview_item.xml b/res/layout/image_preview_item.xml
new file mode 100644
index 0000000..0f6ba43
--- /dev/null
+++ b/res/layout/image_preview_item.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <ImageView android:id="@+id/image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"/>
+ <LinearLayout
+ android:id="@+id/icon_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_horizontal"
+ android:orientation="horizontal"
+ android:paddingLeft="30dp"
+ android:paddingRight="40dp"
+ android:paddingTop="40dp" >
+ </LinearLayout>
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/item_chooser_browse_font.xml b/res/layout/item_chooser_browse_font.xml
new file mode 100644
index 0000000..50744f8
--- /dev/null
+++ b/res/layout/item_chooser_browse_font.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="248dp"
+ android:orientation="vertical"
+ tools:context=".StoreActivity"
+ tools:ignore="MergeRootFrame" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="4"
+ android:paddingLeft="24dp"
+ android:paddingTop="20dp"
+ android:paddingBottom="20dp"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:background="@color/offwhite" >s
+
+ <TextView
+ android:id="@+id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:singleLine="true"
+ android:text="@string/font_preview_letters"
+ android:textSize="36sp" />
+
+ <TextView
+ android:id="@+id/text2"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:singleLine="true"
+ android:text="@string/font_preview_letters"
+ android:textSize="36sp" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="3"
+ android:orientation="vertical"
+ android:gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="-20dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:paddingLeft="24dp"
+ android:fontFamily="sans-serif-light"
+ android:textSize="38sp"
+ android:includeFontPadding="false" />
+
+ <TextView
+ android:id="@+id/author"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="-2dp"
+ android:textColor="@color/author_grey"
+ android:textStyle="bold"
+ android:paddingLeft="26dp"
+ android:textSize="12sp" />
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/item_store_browse.xml b/res/layout/item_store_browse.xml
new file mode 100644
index 0000000..739447b
--- /dev/null
+++ b/res/layout/item_store_browse.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/item_browse_height"
+ android:orientation="vertical"
+ tools:context=".StoreActivity"
+ tools:ignore="MergeRootFrame" >
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="5"
+ android:background="@color/offwhite">
+ <ImageView android:id="@+id/image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"/>
+ <LinearLayout
+ android:id="@+id/icon_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:paddingLeft="30dp"
+ android:paddingRight="30dp">
+ </LinearLayout>
+ </FrameLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="2"
+ android:background="@color/title_author_background" >
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="220sp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:paddingLeft="24dp"
+ android:paddingTop="6dp"
+ android:textSize="28sp" />
+
+ <TextView
+ android:id="@+id/author"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@id/title"
+ android:layout_marginTop="-2dp"
+ android:paddingLeft="26dp"
+ android:textSize="12sp" />
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml
new file mode 100644
index 0000000..93d02de
--- /dev/null
+++ b/res/values-land/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<resources>
+ <dimen name="sliding_up_panel_parallax_distance">75dp</dimen>
+</resources> \ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
new file mode 100644
index 0000000..0c474be
--- /dev/null
+++ b/res/values/attrs.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<resources>
+
+ <!-- Attributes for Sliding Panel -->
+ <declare-styleable name="SlidingUpPanelLayout">
+ <attr name="collapsedHeight" format="dimension" />
+ <attr name="shadowHeight" format="dimension" />
+ <attr name="fadeColor" format="color" />
+ <attr name="flingVelocity" format="integer" />
+ <attr name="dragView" format="reference" />
+ <attr name="startExpanded" format="boolean" />
+ <attr name="anchorPoint" format="float" />
+ <attr name="parallaxDistance" format="dimension" />
+ </declare-styleable>
+
+ <!-- Attributes for Drawer List -->
+ <declare-styleable name="DrawerItem">
+ <attr name="id" format="reference" />
+ <attr name="title" format="string" />
+ <attr name="icon" format="reference" />
+ <attr name="fragmentName" format="string" />
+ <attr name="component" format="string" />
+ </declare-styleable>
+
+</resources> \ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 0000000..e032555
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<resources>
+ <color name="detailedview_pager_background">#ffffff</color>
+ <color name="title_author_background">#ffffff</color>
+ <color name="offwhite">#e9e8e8</color>
+ <color name="author_grey">#808184</color>
+
+ <color name="apply_button_text_color_enabled">#ffffff</color>
+ <color name="apply_button_text_color_disabled">#bbbbbb</color>
+
+ <color name="apply_button_progress_color">#6bd1e9</color>
+ <color name="apply_button_default_color">#00b1e5</color>
+ <color name="apply_button_pressed_color">#008ef2</color>
+</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
new file mode 100644
index 0000000..38e2f4b
--- /dev/null
+++ b/res/values/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<resources>
+ <dimen name="app_icon_size">24dp</dimen>
+ <dimen name="button_bar_height">62dip</dimen>
+ <dimen name="sliding_up_panel_parallax_distance">150dp</dimen>
+ <dimen name="item_browse_height">260dp</dimen>
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..481b4c9
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The CyanogenMod 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.
+-->
+<resources>
+
+ <string name="app_name">Themes</string>
+
+ <!-- Themable items -->
+ <string name="lock_screen">Lock screen</string>
+ <string name="icons">Icons</string>
+ <string name="fonts">Fonts</string>
+ <string name="wallpapers">Wallpapers</string>
+ <string name="boot_anims">Boot animations</string>
+ <string name="ringtones">Ringtones</string>
+ <string name="notifications">Notifications</string>
+ <string name="alarms">Alarms</string>
+ <string name="style">Style</string>
+
+ <string name="apply">Apply</string>
+ <string name="applying">Applying</string>
+ <string name="remove">Remove</string>
+ <string name="update">Update</string>
+
+ <!-- Audibles preview -->
+ <string name="alarm_label">Alarm</string>
+ <string name="notification_label">Notification</string>
+ <string name="ringtone_label">Ringtone</string>
+
+ <string name="unknown_author">Author unknown</string>
+
+ <string name="font_preview_letters">AaBbCcDd123</string>
+
+ <string name="no_boot_animation_preview">No preview available</string>
+
+ <string name="theme_installed_notification_title">%s installed</string>
+ <string name="theme_installed_notification_text">Theme successfully installed.</string>
+
+</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
new file mode 100644
index 0000000..bcf0dba
--- /dev/null
+++ b/res/values/styles.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 The CyanogenMod 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="android:style/Theme.Holo.Light">
+ <item name="android:actionBarStyle">@style/ThemeChooserActionBar</item>
+ <item name="android:actionBarTabTextStyle">@style/ThemeChooserActionBarTabText</item>
+ <item name="android:actionMenuTextColor">#333333</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowActionBarOverlay">false</item>
+ </style>
+
+ <style name="ThemeChooserActionBar" parent="@android:style/Widget.Holo.Light.ActionBar">
+ <item name="android:displayOptions">useLogo|showHome|showTitle</item>
+ <item name="android:background">#BB000000</item>
+ <item name="android:titleTextStyle">@style/ThemeChooserActionBarTitleText</item>
+ <item name="android:logo">@drawable/ic_app_themes_bw</item>
+ </style>
+
+ <!-- ActionBar title text -->
+ <style name="ThemeChooserActionBarTitleText">
+ <item name="android:textColor">#FFFFFF</item>
+ </style>
+
+ <!-- ActionBar tabs text styles -->
+ <style name="ThemeChooserActionBarTabText">
+ <item name="android:textColor">#FFFFFF</item>
+ </style>
+
+ <style name="mixnmatch_menu_btn" parent="@android:style/Widget.Holo.Light.Button.Borderless.Small">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">103dp</item>
+ <item name="android:background">#d2d2c8</item>
+ <item name="android:padding">8dp</item>
+ </style>
+
+
+ <style name="mixnmatch_menu_btn_icon">
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:layout_width">20dp</item>
+ <item name="android:layout_marginRight">8dp</item>
+ <item name="android:layout_alignParentTop">true</item>
+ <item name="android:layout_alignParentBottom">true</item>
+ </style>
+
+
+ <style name="mixnmatch_menu_firstline">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">#3e3e3e</item>
+ </style>
+
+ <style name="mixnmatch_menu_secondline">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#656666</item>
+ </style>
+
+ <style name="mixnmatch_menu_btn_left" parent="@style/mixnmatch_menu_btn">
+ <item name="android:layout_marginRight">4dp</item>
+ </style>
+
+ <style name="mixnmatch_menu_btn_right" parent="@style/mixnmatch_menu_btn">
+ <item name="android:layout_marginRight">4dp</item>
+ </style>
+
+ <style name="drawer_footer">
+ <item name="android:background">#75e5bb</item>
+ <item name="android:textColor">#FFFFFF</item>
+ <item name="android:gravity">left</item>
+ <item name="android:textSize">24sp</item>
+ <item name="android:paddingLeft">10dp</item>
+ <item name="android:paddingTop">20dp</item>
+ <item name="android:paddingBottom">20dp</item>
+ </style>
+
+ <style name="drawer_header">
+ <item name="android:background">#19d38e</item>
+ <item name="android:textColor">#FFFFFF</item>
+ <item name="android:gravity">left</item>
+ <item name="android:textSize">30sp</item>
+ <item name="android:paddingLeft">10dp</item>
+ <item name="android:paddingTop">10dp</item>
+ <item name="android:paddingBottom">10dp</item>
+ </style>
+
+ <style name="drawer_list_item">
+ <item name="android:background">#19d38e</item>
+ <item name="android:gravity">left</item>
+ <item name="android:textSize">30sp</item>
+ <item name="android:paddingLeft">10dp</item>
+ </style>
+
+ <style name="drawer_list_item_text">
+ <item name="android:textColor">#FFFFFF</item>
+ </style>
+
+</resources>
diff --git a/src/com/sothree/slidinguppanel/SlidingupPanelLayout.java b/src/com/sothree/slidinguppanel/SlidingupPanelLayout.java
new file mode 100644
index 0000000..7b277d5
--- /dev/null
+++ b/src/com/sothree/slidinguppanel/SlidingupPanelLayout.java
@@ -0,0 +1,1282 @@
+/*
+ * THIS FILE HAS BEEN MODIFIED BY THE CYANOGENMOD PROJECT
+ *
+ * ORIGINAL SOURCE: https://github.com/umano/AndroidSlidingUpPanel
+ *
+ * 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.sothree.slidinguppanel;
+
+import org.cyanogenmod.theme.chooser.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.ViewDragHelper;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+
+public class SlidingupPanelLayout extends ViewGroup {
+
+ private static final String TAG = SlidingupPanelLayout.class.getSimpleName();
+
+ /**
+ * Default peeking out panel height
+ */
+ private static final int DEFAULT_PANEL_HEIGHT = 68; // dp;
+
+ /**
+ * Default height of the shadow above the peeking out panel
+ */
+ private static final int DEFAULT_SHADOW_HEIGHT = 4; // dp;
+
+ /**
+ * If no fade color is given by default it will fade to 80% gray.
+ */
+ private static final int DEFAULT_FADE_COLOR = 0x99000000;
+
+ /**
+ * Default Minimum velocity that will be detected as a fling
+ */
+ private static final int DEFAULT_MIN_FLING_VELOCITY = 400; // dips per second
+
+ /**
+ * Default attributes for layout
+ */
+ private static final int[] DEFAULT_ATTRS = new int[] {
+ android.R.attr.layout_gravity
+ };
+
+ /**
+ * Minimum velocity that will be detected as a fling
+ */
+ private int mMinFlingVelocity = DEFAULT_MIN_FLING_VELOCITY;
+
+ /**
+ * The fade color used for the panel covered by the slider. 0 = no fading.
+ */
+ private int mCoveredFadeColor = DEFAULT_FADE_COLOR;
+
+ /**
+ * The paint used to dim the main layout when sliding
+ */
+ private final Paint mCoveredFadePaint = new Paint();
+
+ /**
+ * Drawable used to draw the shadow between panes.
+ */
+ private Drawable mShadowDrawable;
+
+ /**
+ * The size of the overhang in pixels.
+ */
+ private int mPanelHeight = -1;
+
+ /**
+ * The size of the shadow in pixels.
+ */
+ private int mShadowHeight = -1;
+
+ /**
+ * True if the collapsed panel should be dragged up.
+ */
+ private boolean mIsSlidingUp;
+
+ /**
+ * True if a panel can slide with the current measurements
+ */
+ private boolean mCanSlide;
+
+ /**
+ * If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be
+ * used for dragging.
+ */
+ private View mDragView;
+
+ /**
+ * If provided, the panel can be dragged by only this view. Otherwise, the entire panel can be
+ * used for dragging.
+ */
+ private int mDragViewResId = -1;
+
+ /**
+ * The child view that can slide, if any.
+ */
+ private View mSlideableView;
+
+ /**
+ * Current state of the slideable view.
+ */
+ private enum SlideState {
+ EXPANDED,
+ COLLAPSED,
+ ANCHORED
+ }
+ private SlideState mSlideState = SlideState.COLLAPSED;
+
+ /**
+ * How far the panel is offset from its expanded position.
+ * range [0, 1] where 0 = expanded, 1 = collapsed.
+ */
+ private float mSlideOffset;
+
+ /**
+ * How far in pixels the slideable panel may move.
+ */
+ private int mSlideRange;
+
+ /**
+ * A panel view is locked into internal scrolling or another condition that
+ * is preventing a drag.
+ */
+ private boolean mIsUnableToDrag;
+
+ /**
+ * Flag indicating that sliding feature is enabled\disabled
+ */
+ private boolean mIsSlidingEnabled;
+
+ /**
+ * Flag indicating if a drag view can have its own touch events. If set
+ * to true, a drag view can scroll horizontally and have its own click listener.
+ *
+ * Default is set to false.
+ */
+ private boolean mIsUsingDragViewTouchEvents;
+
+ /**
+ * Threshold to tell if there was a scroll touch event.
+ */
+ private final int mScrollTouchSlop;
+
+ private float mInitialMotionX;
+ private float mInitialMotionY;
+ private float mAnchorPoint = 0.0f;
+
+ private PanelSlideListener mPanelSlideListener;
+
+ private final ViewDragHelper mDragHelper;
+
+ /**
+ * How far the non-sliding panel is parallaxed from its usual position when open.
+ * range [0, 1]
+ */
+ private float mParallaxOffset;
+
+ /**
+ * Distance in pixels to parallax the fixed pane by when fully closed
+ */
+ private int mParallaxBy;
+
+ /**
+ * Stores whether or not the pane was expanded the last time it was slideable.
+ * If expand/collapse operations are invoked this state is modified. Used by
+ * instance state save/restore.
+ */
+ private boolean mFirstLayout = true;
+
+ private final Rect mTmpRect = new Rect();
+
+ /**
+ * Listener for monitoring events about sliding panes.
+ */
+ public interface PanelSlideListener {
+ /**
+ * Called when a sliding pane's position changes.
+ * @param panel The child view that was moved
+ * @param slideOffset The new offset of this sliding pane within its range, from 0-1
+ */
+ public void onPanelSlide(View panel, float slideOffset);
+ /**
+ * Called when a sliding pane becomes slid completely collapsed. The pane may or may not
+ * be interactive at this point depending on if it's shown or hidden
+ * @param panel The child view that was slid to an collapsed position, revealing other panes
+ */
+ public void onPanelCollapsed(View panel);
+
+ /**
+ * Called when a sliding pane becomes slid completely expanded. The pane is now guaranteed
+ * to be interactive. It may now obscure other views in the layout.
+ * @param panel The child view that was slid to a expanded position
+ */
+ public void onPanelExpanded(View panel);
+
+ public void onPanelAnchored(View panel);
+ }
+
+ /**
+ * No-op stubs for {@link PanelSlideListener}. If you only want to implement a subset
+ * of the listener methods you can extend this instead of implement the full interface.
+ */
+ public static class SimplePanelSlideListener implements PanelSlideListener {
+ @Override
+ public void onPanelSlide(View panel, float slideOffset) {
+ }
+ @Override
+ public void onPanelCollapsed(View panel) {
+ }
+ @Override
+ public void onPanelExpanded(View panel) {
+ }
+ @Override
+ public void onPanelAnchored(View panel) {
+ }
+ }
+
+ public SlidingupPanelLayout(Context context) {
+ this(context, null);
+ }
+
+ public SlidingupPanelLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SlidingupPanelLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ if (attrs != null) {
+ TypedArray defAttrs = context.obtainStyledAttributes(attrs, DEFAULT_ATTRS);
+
+ if (defAttrs != null) {
+ int gravity = defAttrs.getInt(0, Gravity.NO_GRAVITY);
+ if (gravity != Gravity.TOP && gravity != Gravity.BOTTOM) {
+ throw new IllegalArgumentException("layout_gravity must be set to either top or bottom");
+ }
+ mIsSlidingUp = gravity == Gravity.BOTTOM;
+ }
+
+ defAttrs.recycle();
+
+ TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlidingUpPanelLayout);
+
+ if (ta != null) {
+ mPanelHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_collapsedHeight, -1);
+ mShadowHeight = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_shadowHeight, -1);
+
+ mMinFlingVelocity = ta.getInt(R.styleable.SlidingUpPanelLayout_flingVelocity, DEFAULT_MIN_FLING_VELOCITY);
+ mCoveredFadeColor = ta.getColor(R.styleable.SlidingUpPanelLayout_fadeColor, DEFAULT_FADE_COLOR);
+
+ mDragViewResId = ta.getResourceId(R.styleable.SlidingUpPanelLayout_dragView, -1);
+ mParallaxBy = ta.getDimensionPixelSize(R.styleable.SlidingUpPanelLayout_parallaxDistance, 0);
+ boolean startExpanded = ta.getBoolean(R.styleable.SlidingUpPanelLayout_startExpanded, false);
+ mAnchorPoint = ta.getFloat(R.styleable.SlidingUpPanelLayout_anchorPoint, 0.0f);
+ if (startExpanded) {
+ mSlideState = (mAnchorPoint != 0) ? SlideState.ANCHORED : SlideState.EXPANDED;
+ }
+ }
+
+ ta.recycle();
+ }
+
+ final float density = context.getResources().getDisplayMetrics().density;
+ if (mPanelHeight == -1) {
+ mPanelHeight = (int) (DEFAULT_PANEL_HEIGHT * density + 0.5f);
+ }
+ if (mShadowHeight == -1) {
+ mShadowHeight = (int) (DEFAULT_SHADOW_HEIGHT * density + 0.5f);
+ }
+
+ setWillNotDraw(false);
+
+ mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback());
+ mDragHelper.setMinVelocity(mMinFlingVelocity * density);
+
+ mCanSlide = true;
+ mIsSlidingEnabled = true;
+
+ ViewConfiguration vc = ViewConfiguration.get(context);
+ mScrollTouchSlop = vc.getScaledTouchSlop();
+ }
+
+ /**
+ * Set the Drag View after the view is inflated
+ */
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ if (mDragViewResId != -1) {
+ mDragView = findViewById(mDragViewResId);
+ }
+ }
+
+ /**
+ * Set a distance to parallax the lower pane by when the upper pane is in its
+ * fully closed state. The lower pane will scroll between this position and
+ * its fully open state.
+ *
+ * @param parallaxBy Distance to parallax by in pixels
+ */
+ public void setParallaxDistance(int parallaxBy) {
+ mParallaxBy = parallaxBy;
+ requestLayout();
+ }
+
+ /**
+ * @return The distance the lower pane will parallax by when the upper pane is fully closed.
+ *
+ * @see #setParallaxDistance(int)
+ */
+ public int getParallaxDistance() {
+ return mParallaxBy;
+ }
+
+ /**
+ * Set the color used to fade the pane covered by the sliding pane out when the pane
+ * will become fully covered in the expanded state.
+ *
+ * @param color An ARGB-packed color value
+ */
+ public void setCoveredFadeColor(int color) {
+ mCoveredFadeColor = color;
+ invalidate();
+ }
+
+ /**
+ * @return The ARGB-packed color value used to fade the fixed pane
+ */
+ public int getCoveredFadeColor() {
+ return mCoveredFadeColor;
+ }
+
+ /**
+ * Set the collapsed panel height in pixels
+ *
+ * @param val A height in pixels
+ */
+ public void setPanelHeight(int val) {
+ mPanelHeight = val;
+ requestLayout();
+ }
+
+ /**
+ * @return The current collapsed panel height
+ */
+ public int getPanelHeight() {
+ return mPanelHeight;
+ }
+
+ public void setPanelSlideListener(PanelSlideListener listener) {
+ mPanelSlideListener = listener;
+ }
+
+ /**
+ * Set the draggable view portion. Use to null, to allow the whole panel to be draggable
+ *
+ * @param dragView A view that will be used to drag the panel.
+ */
+ public void setDragView(View dragView) {
+ mDragView = dragView;
+ }
+
+ /**
+ * Set an anchor point where the panel can stop during sliding
+ *
+ * @param anchorPoint A value between 0 and 1, determining the position of the anchor point
+ * starting from the top of the layout.
+ */
+ public void setAnchorPoint(float anchorPoint) {
+ if (anchorPoint > 0 && anchorPoint < 1)
+ mAnchorPoint = anchorPoint;
+ }
+
+ public float getAnchorPoint() {
+ return mAnchorPoint;
+ }
+
+ /**
+ * Set the shadow for the sliding panel
+ *
+ */
+ public void setShadowDrawable(Drawable drawable) {
+ mShadowDrawable = drawable;
+ }
+
+ void dispatchOnPanelSlide(View panel) {
+ if (mPanelSlideListener != null) {
+ mPanelSlideListener.onPanelSlide(panel, mSlideOffset);
+ }
+ }
+
+ void dispatchOnPanelExpanded(View panel) {
+ if (mPanelSlideListener != null) {
+ mPanelSlideListener.onPanelExpanded(panel);
+ }
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+
+ void dispatchOnPanelCollapsed(View panel) {
+ if (mPanelSlideListener != null) {
+ mPanelSlideListener.onPanelCollapsed(panel);
+ }
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+
+ void dispatchOnPanelAnchored(View panel) {
+ if (mPanelSlideListener != null) {
+ mPanelSlideListener.onPanelAnchored(panel);
+ }
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+
+ void updateObscuredViewVisibility() {
+ if (getChildCount() == 0) {
+ return;
+ }
+ final int leftBound = getPaddingLeft();
+ final int rightBound = getWidth() - getPaddingRight();
+ final int topBound = getPaddingTop();
+ final int bottomBound = getHeight() - getPaddingBottom();
+ final int left;
+ final int right;
+ final int top;
+ final int bottom;
+ if (mSlideableView != null && hasOpaqueBackground(mSlideableView)) {
+ left = mSlideableView.getLeft();
+ right = mSlideableView.getRight();
+ top = mSlideableView.getTop();
+ bottom = mSlideableView.getBottom();
+ } else {
+ left = right = top = bottom = 0;
+ }
+ View child = getChildAt(0);
+ final int clampedChildLeft = Math.max(leftBound, child.getLeft());
+ final int clampedChildTop = Math.max(topBound, child.getTop());
+ final int clampedChildRight = Math.min(rightBound, child.getRight());
+ final int clampedChildBottom = Math.min(bottomBound, child.getBottom());
+ final int vis;
+ if (clampedChildLeft >= left && clampedChildTop >= top &&
+ clampedChildRight <= right && clampedChildBottom <= bottom) {
+ vis = INVISIBLE;
+ } else {
+ vis = VISIBLE;
+ }
+ child.setVisibility(vis);
+ }
+
+ void setAllChildrenVisible() {
+ for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == INVISIBLE) {
+ child.setVisibility(VISIBLE);
+ }
+ }
+ }
+
+ private static boolean hasOpaqueBackground(View v) {
+ final Drawable bg = v.getBackground();
+ if (bg != null) {
+ return bg.getOpacity() == PixelFormat.OPAQUE;
+ }
+ return false;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mFirstLayout = true;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mFirstLayout = true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthMode != MeasureSpec.EXACTLY) {
+ throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
+ } else if (heightMode != MeasureSpec.EXACTLY) {
+ throw new IllegalStateException("Height must have an exact value or MATCH_PARENT");
+ }
+
+ int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
+ int panelHeight = mPanelHeight;
+
+ final int childCount = getChildCount();
+
+ if (childCount > 2) {
+ Log.e(TAG, "onMeasure: More than two child views are not supported.");
+ } else if (getChildAt(1).getVisibility() == GONE) {
+ panelHeight = 0;
+ }
+
+ // We'll find the current one below.
+ mSlideableView = null;
+ mCanSlide = false;
+
+ // First pass. Measure based on child LayoutParams width/height.
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ int height = layoutHeight;
+ if (child.getVisibility() == GONE) {
+ lp.dimWhenOffset = false;
+ continue;
+ }
+
+ if (i == 1) {
+ lp.slideable = true;
+ lp.dimWhenOffset = true;
+ mSlideableView = child;
+ mCanSlide = true;
+ } else {
+ height -= panelHeight;
+ }
+
+ int childWidthSpec;
+ if (lp.width == LayoutParams.WRAP_CONTENT) {
+ childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
+ } else if (lp.width == LayoutParams.MATCH_PARENT) {
+ childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+ } else {
+ childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
+ }
+
+ int childHeightSpec;
+ if (lp.height == LayoutParams.WRAP_CONTENT) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
+ } else if (lp.height == LayoutParams.MATCH_PARENT) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ } else {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+ }
+
+ child.measure(childWidthSpec, childHeightSpec);
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int paddingLeft = getPaddingLeft();
+ final int paddingTop = getPaddingTop();
+ final int slidingTop = getSlidingTop();
+
+ final int childCount = getChildCount();
+
+ if (mFirstLayout) {
+ switch (mSlideState) {
+ case EXPANDED:
+ mSlideOffset = mCanSlide ? 0.f : 1.f;
+ break;
+ case ANCHORED:
+ mSlideOffset = mCanSlide ? mAnchorPoint : 1.f;
+ break;
+ default:
+ mSlideOffset = 1.f;
+ break;
+ }
+ }
+
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int childHeight = child.getMeasuredHeight();
+ int offset = 0;
+
+ if (lp.slideable) {
+ mSlideRange = childHeight - mPanelHeight;
+ } else if (mCanSlide && mParallaxBy != 0) {
+ offset = (int) ((1 - mSlideOffset) * mParallaxBy);
+ }
+
+ final int childTop;
+ if (mIsSlidingUp) {
+ childTop = (lp.slideable ? slidingTop + (int) (mSlideRange * mSlideOffset)
+ : paddingTop) - offset;
+ } else {
+ childTop = (lp.slideable ? slidingTop - (int) (mSlideRange * mSlideOffset)
+ : paddingTop + mPanelHeight) + offset;
+ }
+ final int childBottom = childTop + childHeight;
+ final int childLeft = paddingLeft;
+ final int childRight = childLeft + child.getMeasuredWidth();
+
+ child.layout(childLeft, childTop, childRight, childBottom);
+ }
+
+ if (mFirstLayout) {
+ if (mCanSlide) {
+ if (mParallaxBy != 0) {
+ parallaxOtherViews(mSlideOffset);
+ }
+ }
+ updateObscuredViewVisibility();
+ }
+
+ mFirstLayout = false;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ // Recalculate sliding panes and their details
+ if (h != oldh) {
+ mFirstLayout = true;
+ }
+ }
+
+ /**
+ * Set sliding enabled flag
+ * @param enabled flag value
+ */
+ public void setSlidingEnabled(boolean enabled) {
+ mIsSlidingEnabled = enabled;
+ }
+
+ /**
+ * Set if the drag view can have its own touch events. If set
+ * to true, a drag view can scroll horizontally and have its own click listener.
+ *
+ * Default is set to false.
+ */
+ public void setEnableDragViewTouchEvents(boolean enabled) {
+ mIsUsingDragViewTouchEvents = enabled;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ final int action = MotionEventCompat.getActionMasked(ev);
+
+ if (!mCanSlide || !mIsSlidingEnabled || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) {
+ mDragHelper.cancel();
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ mDragHelper.cancel();
+ return false;
+ }
+
+ final float x = ev.getX();
+ final float y = ev.getY();
+ boolean interceptTap = false;
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ mIsUnableToDrag = false;
+ mInitialMotionX = x;
+ mInitialMotionY = y;
+ if (isDragViewUnder((int) x, (int) y) && !mIsUsingDragViewTouchEvents) {
+ interceptTap = true;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ final float adx = Math.abs(x - mInitialMotionX);
+ final float ady = Math.abs(y - mInitialMotionY);
+ final int dragSlop = mDragHelper.getTouchSlop();
+
+ // Handle any horizontal scrolling on the drag view.
+ if (mIsUsingDragViewTouchEvents) {
+ if (adx > mScrollTouchSlop && ady < mScrollTouchSlop) {
+ return super.onInterceptTouchEvent(ev);
+ }
+ // Intercept the touch if the drag view has any vertical scroll.
+ // onTouchEvent will determine if the view should drag vertically.
+ else if (ady > mScrollTouchSlop) {
+ interceptTap = isDragViewUnder((int) x, (int) y);
+ }
+ }
+
+ if ((ady > dragSlop && adx > ady) || !isDragViewUnder((int) x, (int) y)) {
+ mDragHelper.cancel();
+ mIsUnableToDrag = true;
+ return false;
+ }
+ break;
+ }
+ }
+
+ final boolean interceptForDrag = mDragHelper.shouldInterceptTouchEvent(ev);
+
+ return interceptForDrag;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (!mCanSlide || !mIsSlidingEnabled) {
+ return super.onTouchEvent(ev);
+ }
+
+ mDragHelper.processTouchEvent(ev);
+
+ final int action = ev.getAction();
+ boolean wantTouchEvents = true;
+
+ switch (action & MotionEventCompat.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ mInitialMotionX = x;
+ mInitialMotionY = y;
+ break;
+ }
+
+ case MotionEvent.ACTION_UP: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ final float dx = x - mInitialMotionX;
+ final float dy = y - mInitialMotionY;
+ final int slop = mDragHelper.getTouchSlop();
+ View dragView = mDragView != null ? mDragView : mSlideableView;
+ if (dx * dx + dy * dy < slop * slop &&
+ isDragViewUnder((int) x, (int) y)) {
+ dragView.playSoundEffect(SoundEffectConstants.CLICK);
+ if (!isExpanded() && !isAnchored()) {
+ expandPane(mAnchorPoint);
+ } else if (!isAnchored()) {
+ expandPane(mAnchorPoint);
+ } else {
+ collapsePane();
+ }
+ break;
+ }
+ break;
+ }
+ }
+
+ return wantTouchEvents;
+ }
+
+ private boolean isDragViewUnder(int x, int y) {
+ View dragView = mDragView != null ? mDragView : mSlideableView;
+ if (dragView == null) return false;
+ int[] viewLocation = new int[2];
+ dragView.getLocationOnScreen(viewLocation);
+ int[] parentLocation = new int[2];
+ this.getLocationOnScreen(parentLocation);
+ int screenX = parentLocation[0] + x;
+ int screenY = parentLocation[1] + y;
+ return screenX >= viewLocation[0] && screenX < viewLocation[0] + dragView.getWidth() &&
+ screenY >= viewLocation[1] && screenY < viewLocation[1] + dragView.getHeight();
+ }
+
+ private boolean expandPane(View pane, int initialVelocity, float mSlideOffset) {
+ if (mFirstLayout || smoothSlideTo(mSlideOffset, initialVelocity)) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean collapsePane(View pane, int initialVelocity) {
+ if (mFirstLayout || smoothSlideTo(1.f, initialVelocity)) {
+ return true;
+ }
+ return false;
+ }
+
+ private int getSlidingTop() {
+ if (mSlideableView != null) {
+ return getMeasuredHeight() - getPaddingBottom() - mSlideableView.getMeasuredHeight();
+ }
+
+ return getMeasuredHeight() - getPaddingBottom();
+ }
+
+ /**
+ * Collapse the sliding pane if it is currently slideable. If first layout
+ * has already completed this will animate.
+ *
+ * @return true if the pane was slideable and is now collapsed/in the process of collapsing
+ */
+ public boolean collapsePane() {
+ return collapsePane(mSlideableView, 0);
+ }
+
+ /**
+ * Expand the sliding pane if it is currently slideable. If first layout
+ * has already completed this will animate.
+ *
+ * @return true if the pane was slideable and is now expanded/in the process of expading
+ */
+ public boolean expandPane() {
+ return expandPane(0);
+ }
+
+ /**
+ * Partially expand the sliding pane up to a specific offset
+ *
+ * @param mSlideOffset Value between 0 and 1, where 0 is completely expanded.
+ * @return true if the pane was slideable and is now expanded/in the process of expading
+ */
+ public boolean expandPane(float mSlideOffset) {
+ if (!isPaneVisible()) {
+ showPane();
+ }
+ return expandPane(mSlideableView, 0, mSlideOffset);
+ }
+
+ /**
+ * Check if the layout is completely expanded.
+ *
+ * @return true if sliding panels are completely expanded
+ */
+ public boolean isExpanded() {
+ return mSlideState == SlideState.EXPANDED;
+ }
+
+ /**
+ * Check if the layout is anchored in an intermediate point.
+ *
+ * @return true if sliding panels are anchored
+ */
+ public boolean isAnchored() {
+ return mSlideState == SlideState.ANCHORED;
+ }
+
+ /**
+ * Check if the content in this layout cannot fully fit side by side and therefore
+ * the content pane can be slid back and forth.
+ *
+ * @return true if content in this layout can be expanded
+ */
+ public boolean isSlideable() {
+ return mCanSlide;
+ }
+
+ public boolean isPaneVisible() {
+ if (getChildCount() < 2) {
+ return false;
+ }
+ View slidingPane = getChildAt(1);
+ return slidingPane.getVisibility() == View.VISIBLE;
+ }
+
+ public void showPane() {
+ if (getChildCount() < 2) {
+ return;
+ }
+ View slidingPane = getChildAt(1);
+ slidingPane.setVisibility(View.VISIBLE);
+ requestLayout();
+ }
+
+ public void hidePane() {
+ if (mSlideableView == null) {
+ return;
+ }
+ mSlideableView.setVisibility(View.GONE);
+ requestLayout();
+ }
+
+ private void onPanelDragged(int newTop) {
+ final int topBound = getSlidingTop();
+ mSlideOffset = mIsSlidingUp
+ ? (float) (newTop - topBound) / mSlideRange
+ : (float) (topBound - newTop) / mSlideRange;
+ if (mParallaxBy != 0) {
+ parallaxOtherViews(mSlideOffset);
+ }
+ dispatchOnPanelSlide(mSlideableView);
+ }
+
+ @Override
+ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ boolean result;
+ final int save = canvas.save(Canvas.CLIP_SAVE_FLAG);
+
+ boolean drawScrim = false;
+
+ if (mCanSlide && !lp.slideable && mSlideableView != null) {
+ // Clip against the slider; no sense drawing what will immediately be covered.
+ canvas.getClipBounds(mTmpRect);
+ if (mIsSlidingUp) {
+ mTmpRect.bottom = Math.min(mTmpRect.bottom, mSlideableView.getTop());
+ } else {
+ mTmpRect.top = Math.max(mTmpRect.top, mSlideableView.getBottom());
+ }
+ canvas.clipRect(mTmpRect);
+ if (mSlideOffset < 1) {
+ drawScrim = true;
+ }
+ }
+
+ result = super.drawChild(canvas, child, drawingTime);
+ canvas.restoreToCount(save);
+
+ if (drawScrim) {
+ final int baseAlpha = (mCoveredFadeColor & 0xff000000) >>> 24;
+ final int imag = (int) (baseAlpha * (1 - mSlideOffset));
+ final int color = imag << 24 | (mCoveredFadeColor & 0xffffff);
+ mCoveredFadePaint.setColor(color);
+ canvas.drawRect(mTmpRect, mCoveredFadePaint);
+ }
+
+ return result;
+ }
+
+ /**
+ * Smoothly animate mDraggingPane to the target X position within its range.
+ *
+ * @param slideOffset position to animate to
+ * @param velocity initial velocity in case of fling, or 0.
+ */
+ boolean smoothSlideTo(float slideOffset, int velocity) {
+ if (!mCanSlide) {
+ // Nothing to do.
+ return false;
+ }
+
+ final int topBound = getSlidingTop();
+ int y = mIsSlidingUp
+ ? (int) (topBound + slideOffset * mSlideRange)
+ : (int) (topBound - slideOffset * mSlideRange);
+
+ if (mDragHelper.smoothSlideViewTo(mSlideableView, mSlideableView.getLeft(), y)) {
+ setAllChildrenVisible();
+ ViewCompat.postInvalidateOnAnimation(this);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mDragHelper.continueSettling(true)) {
+ if (!mCanSlide) {
+ mDragHelper.abort();
+ return;
+ }
+
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+
+ @Override
+ public void draw(Canvas c) {
+ super.draw(c);
+
+ if (mSlideableView == null) {
+ // No need to draw a shadow if we don't have one.
+ return;
+ }
+
+ final int right = mSlideableView.getRight();
+ final int top;
+ final int bottom;
+ if (mIsSlidingUp) {
+ top = mSlideableView.getTop() - mShadowHeight;
+ bottom = mSlideableView.getTop();
+ } else {
+ top = mSlideableView.getBottom();
+ bottom = mSlideableView.getBottom() + mShadowHeight;
+ }
+ final int left = mSlideableView.getLeft();
+
+ if (mShadowDrawable != null) {
+ mShadowDrawable.setBounds(left, top, right, bottom);
+ mShadowDrawable.draw(c);
+ }
+ }
+
+ private void parallaxOtherViews(float slideOffset) {
+ final LayoutParams slideLp = (LayoutParams) mSlideableView.getLayoutParams();
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View v = getChildAt(i);
+ if (v == mSlideableView) continue;
+
+ final int oldOffset = (int) ((1 - mParallaxOffset) * mParallaxBy);
+ mParallaxOffset = slideOffset;
+ final int newOffset = (int) ((1 - slideOffset) * mParallaxBy);
+ final int dx = mIsSlidingUp ? oldOffset - newOffset : newOffset - oldOffset;
+
+ v.offsetTopAndBottom(dx);
+ }
+ }
+
+ /**
+ * Tests scrollability within child views of v given a delta of dx.
+ *
+ * @param v View to test for horizontal scrollability
+ * @param checkV Whether the view v passed should itself be checked for scrollability (true),
+ * or just its children (false).
+ * @param dx Delta scrolled in pixels
+ * @param x X coordinate of the active touch point
+ * @param y Y coordinate of the active touch point
+ * @return true if child views of v can be scrolled by delta of dx.
+ */
+ protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
+ if (v instanceof ViewGroup) {
+ final ViewGroup group = (ViewGroup) v;
+ final int scrollX = v.getScrollX();
+ final int scrollY = v.getScrollY();
+ final int count = group.getChildCount();
+ // Count backwards - let topmost views consume scroll distance first.
+ for (int i = count - 1; i >= 0; i--) {
+ final View child = group.getChildAt(i);
+ if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
+ y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
+ canScroll(child, true, dx, x + scrollX - child.getLeft(),
+ y + scrollY - child.getTop())) {
+ return true;
+ }
+ }
+ }
+ return checkV && ViewCompat.canScrollHorizontally(v, -dx);
+ }
+
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams();
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof MarginLayoutParams
+ ? new LayoutParams((MarginLayoutParams) p)
+ : new LayoutParams(p);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams && super.checkLayoutParams(p);
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ SavedState ss = new SavedState(superState);
+ ss.mSlideState = mSlideState;
+
+ return ss;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mSlideState = ss.mSlideState;
+ }
+
+ private class DragHelperCallback extends ViewDragHelper.Callback {
+
+ @Override
+ public boolean tryCaptureView(View child, int pointerId) {
+ if (mIsUnableToDrag) {
+ return false;
+ }
+
+ return ((LayoutParams) child.getLayoutParams()).slideable;
+ }
+
+ @Override
+ public void onViewDragStateChanged(int state) {
+ int anchoredTop = (int)(mAnchorPoint*mSlideRange);
+
+ if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
+ if (mSlideOffset == 0) {
+ if (mSlideState != SlideState.EXPANDED) {
+ updateObscuredViewVisibility();
+ dispatchOnPanelExpanded(mSlideableView);
+ mSlideState = SlideState.EXPANDED;
+ }
+ } else if (mSlideOffset == (float)anchoredTop/(float)mSlideRange) {
+ if (mSlideState != SlideState.ANCHORED) {
+ updateObscuredViewVisibility();
+ dispatchOnPanelAnchored(mSlideableView);
+ mSlideState = SlideState.ANCHORED;
+ }
+ } else if (mSlideState != SlideState.COLLAPSED) {
+ dispatchOnPanelCollapsed(mSlideableView);
+ mSlideState = SlideState.COLLAPSED;
+ }
+ }
+ }
+
+ @Override
+ public void onViewCaptured(View capturedChild, int activePointerId) {
+ // Make all child views visible in preparation for sliding things around
+ setAllChildrenVisible();
+ }
+
+ @Override
+ public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+ onPanelDragged(top);
+ invalidate();
+ }
+
+ @Override
+ public void onViewReleased(View releasedChild, float xvel, float yvel) {
+ int top = mIsSlidingUp
+ ? getSlidingTop()
+ : getSlidingTop() - mSlideRange;
+
+ if (mAnchorPoint != 0) {
+ int anchoredTop;
+ float anchorOffset;
+ if (mIsSlidingUp) {
+ anchoredTop = (int)(mAnchorPoint*mSlideRange);
+ anchorOffset = (float)anchoredTop/(float)mSlideRange;
+ } else {
+ anchoredTop = mPanelHeight - (int)(mAnchorPoint*mSlideRange);
+ anchorOffset = (float)(mPanelHeight - anchoredTop)/(float)mSlideRange;
+ }
+
+ if (yvel > 0 || (yvel == 0 && mSlideOffset >= (1f+anchorOffset)/2)) {
+ top += mSlideRange;
+ } else if (yvel == 0 && mSlideOffset < (1f+anchorOffset)/2
+ && mSlideOffset >= anchorOffset/2) {
+ top += mSlideRange * mAnchorPoint;
+ }
+
+ } else if (yvel > 0 || (yvel == 0 && mSlideOffset > 0.5f)) {
+ top += mSlideRange;
+ }
+
+ mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
+ invalidate();
+ }
+
+ @Override
+ public int getViewVerticalDragRange(View child) {
+ return mSlideRange;
+ }
+
+ @Override
+ public int clampViewPositionVertical(View child, int top, int dy) {
+ final int topBound;
+ final int bottomBound;
+ if (mIsSlidingUp) {
+ topBound = getSlidingTop();
+ bottomBound = topBound + mSlideRange;
+ } else {
+ bottomBound = getPaddingTop();
+ topBound = bottomBound - mSlideRange;
+ }
+
+ return Math.min(Math.max(top, topBound), bottomBound);
+ }
+ }
+
+ public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+ private static final int[] ATTRS = new int[] {
+ android.R.attr.layout_weight
+ };
+
+ /**
+ * True if this pane is the slideable pane in the layout.
+ */
+ boolean slideable;
+
+ /**
+ * True if this view should be drawn dimmed
+ * when it's been offset from its default position.
+ */
+ boolean dimWhenOffset;
+
+ Paint dimPaint;
+
+ public LayoutParams() {
+ super(MATCH_PARENT, MATCH_PARENT);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(android.view.ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS);
+ a.recycle();
+ }
+
+ }
+
+ static class SavedState extends BaseSavedState {
+ SlideState mSlideState;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ try {
+ mSlideState = Enum.valueOf(SlideState.class, in.readString());
+ } catch (IllegalArgumentException e) {
+ mSlideState = SlideState.COLLAPSED;
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeString(mSlideState.toString());
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+} \ No newline at end of file
diff --git a/src/org/cyanogenmod/theme/chooser/AppReceiver.java b/src/org/cyanogenmod/theme/chooser/AppReceiver.java
new file mode 100644
index 0000000..f31fdbe
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/AppReceiver.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.chooser;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import org.cyanogenmod.theme.util.NotificationHelper;
+
+public class AppReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Uri uri = intent.getData();
+ String pkgName = uri != null ? uri.getSchemeSpecificPart() : null;
+ String action = intent.getAction();
+ boolean isReplacing = intent.getExtras().getBoolean(Intent.EXTRA_REPLACING, false);
+
+ if (Intent.ACTION_PACKAGE_ADDED.equals(action) && !isReplacing) {
+ try {
+ if (isTheme(context, pkgName)) {
+ NotificationHelper.postThemeInstalledNotification(context, pkgName);
+ }
+ } catch (NameNotFoundException e) {
+ }
+ } else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) {
+ NotificationHelper.cancelNotificationForPackage(context, pkgName);
+ }
+ }
+
+ private boolean isTheme(Context context, String pkgName) throws NameNotFoundException {
+ PackageInfo pi = context.getPackageManager().getPackageInfo(pkgName, 0);
+ if (pi == null) return false;
+
+ if ((pi.themeInfos != null && pi.themeInfos.length > 0) ||
+ (pi.legacyThemeInfos != null && pi.legacyThemeInfos.length > 0)) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/AudiblePreviewFragment.java b/src/org/cyanogenmod/theme/chooser/AudiblePreviewFragment.java
new file mode 100644
index 0000000..4942d47
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/AudiblePreviewFragment.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.chooser;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ThemeUtils;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.AssetManager;
+import android.media.MediaPlayer;
+import android.media.RingtoneManager;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.io.File;
+import java.io.IOException;
+
+
+public class AudiblePreviewFragment extends Fragment {
+ private static final String PKG_EXTRA = "pkg_extra";
+
+ private static final LinearLayout.LayoutParams ITEM_LAYOUT_PARAMS =
+ new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ private String mPkgName;
+ private LinearLayout mContent;
+ private SparseArray<MediaPlayer> mMediaPlayers;
+
+ static AudiblePreviewFragment newInstance(String pkgName) {
+ final AudiblePreviewFragment f = new AudiblePreviewFragment();
+ final Bundle args = new Bundle();
+ args.putString(PKG_EXTRA, pkgName);
+ f.setArguments(args);
+ return f;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mPkgName = getArguments().getString(PKG_EXTRA);
+ mMediaPlayers = new SparseArray<MediaPlayer>(3);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.audibles_preview, container, false);
+ mContent = (LinearLayout) v.findViewById(R.id.audibles_layout);
+ return v;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ loadAudibles();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ freeMediaPlayers();
+ }
+
+ @Override
+ public void setUserVisibleHint(boolean isVisibleToUser) {
+ super.setUserVisibleHint(isVisibleToUser);
+ if (!isVisibleToUser) {
+ stopMediaPlayers();
+ }
+ }
+
+ private void freeMediaPlayers() {
+ final int N = mMediaPlayers.size();
+ for (int i = 0; i < N; i++) {
+ MediaPlayer mp = mMediaPlayers.get(mMediaPlayers.keyAt(i));
+ if (mp != null) {
+ mp.stop();
+ mp.release();
+ }
+ }
+ mMediaPlayers.clear();
+ }
+
+ private View.OnClickListener mPlayPauseClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ MediaPlayer mp = (MediaPlayer) v.getTag();
+ if (mp != null) {
+ if (mp.isPlaying()) {
+ ((ImageView) v).setImageResource(android.R.drawable.ic_media_play);
+ mp.pause();
+ mp.seekTo(0);
+ } else {
+ stopMediaPlayers();
+ ((ImageView) v).setImageResource(android.R.drawable.ic_media_pause);
+ mp.start();
+ }
+ }
+ }
+ };
+
+ private MediaPlayer.OnCompletionListener mPlayCompletionListener
+ = new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ final int numChildern = mContent.getChildCount();
+ for (int i = 0; i < numChildern; i++) {
+ ((ImageView) mContent.getChildAt(i).findViewById(R.id.btn_play_pause))
+ .setImageResource(android.R.drawable.ic_media_play);
+ }
+ }
+ };
+
+ private void stopMediaPlayers() {
+ if (mContent == null) return;
+ final int numChildern = mContent.getChildCount();
+ for (int i = 0; i < numChildern; i++) {
+ ImageView iv = (ImageView) mContent.getChildAt(i).findViewById(R.id.btn_play_pause);
+ if (iv != null) {
+ iv.setImageResource(android.R.drawable.ic_media_play);
+ MediaPlayer mp = (MediaPlayer) iv.getTag();
+ if (mp != null && mp.isPlaying()) {
+ mp.pause();
+ mp.seekTo(0);
+ }
+ }
+ }
+ }
+
+ private void loadAudibles() {
+ mContent.removeAllViews();
+ if ("default".equals(mPkgName)) {
+ loadSystemAudible(RingtoneManager.TYPE_ALARM);
+ loadSystemAudible(RingtoneManager.TYPE_NOTIFICATION);
+ loadSystemAudible(RingtoneManager.TYPE_RINGTONE);
+ } else {
+ try {
+ final Context themeCtx = getActivity().createPackageContext(mPkgName, 0);
+ PackageInfo pi = getActivity().getPackageManager().getPackageInfo(mPkgName, 0);
+ loadThemeAudible(themeCtx, RingtoneManager.TYPE_ALARM, pi);
+ loadThemeAudible(themeCtx, RingtoneManager.TYPE_NOTIFICATION, pi);
+ loadThemeAudible(themeCtx, RingtoneManager.TYPE_RINGTONE, pi);
+ } catch (PackageManager.NameNotFoundException e) {
+ return;
+ }
+ }
+ }
+
+ private void loadThemeAudible(Context themeCtx, int type, PackageInfo pi) {
+ if (pi.isLegacyThemeApk) {
+ loadLegacyThemeAudible(themeCtx, type, pi);
+ return;
+ }
+ AssetManager assetManager = themeCtx.getAssets();
+ String assetPath;
+ switch (type) {
+ case RingtoneManager.TYPE_ALARM:
+ assetPath = "alarms";
+ break;
+ case RingtoneManager.TYPE_NOTIFICATION:
+ assetPath = "notifications";
+ break;
+ case RingtoneManager.TYPE_RINGTONE:
+ assetPath = "ringtones";
+ break;
+ default:
+ assetPath = null;
+ break;
+ }
+ if (assetPath != null) {
+ try {
+ String[] assetList = assetManager.list(assetPath);
+ if (assetList != null && assetList.length > 0) {
+ AssetFileDescriptor afd = assetManager.openFd(assetPath
+ + File.separator + assetList[0]);
+ MediaPlayer mp = initAudibleMediaPlayer(afd, type);
+ if (mp != null) {
+ addAudibleToLayout(type, mp);
+ }
+ }
+ } catch (IOException e) {
+ mMediaPlayers.put(type, null);
+ }
+ }
+ }
+
+ private void loadLegacyThemeAudible(Context themeCtx, int type, PackageInfo pi) {
+ if (pi.legacyThemeInfos == null || pi.legacyThemeInfos.length == 0)
+ return;
+ AssetManager assetManager = themeCtx.getAssets();
+ String assetPath;
+ switch (type) {
+ case RingtoneManager.TYPE_NOTIFICATION:
+ assetPath = pi.legacyThemeInfos[0].notificationFileName;
+ break;
+ case RingtoneManager.TYPE_RINGTONE:
+ assetPath = pi.legacyThemeInfos[0].ringtoneFileName;
+ break;
+ default:
+ assetPath = null;
+ break;
+ }
+ if (assetPath != null) {
+ try {
+ AssetFileDescriptor afd = assetManager.openFd(assetPath);
+ MediaPlayer mp = initAudibleMediaPlayer(afd, type);
+ if (mp != null) {
+ addAudibleToLayout(type, mp);
+ }
+ } catch (IOException e) {
+ mMediaPlayers.put(type, null);
+ }
+ }
+ }
+
+ private void loadSystemAudible(int type) {
+ final String audiblePath = ThemeUtils.getDefaultAudiblePath(type);
+ if (audiblePath != null && (new File(audiblePath)).exists()) {
+ try {
+ MediaPlayer mp = initAudibleMediaPlayer(audiblePath, type);
+ addAudibleToLayout(type, mp);
+ } catch (IOException e) {
+ mMediaPlayers.put(type, null);
+ }
+ }
+ }
+
+ private MediaPlayer initAudibleMediaPlayer(String audiblePath, int type) throws IOException {
+ MediaPlayer mp = mMediaPlayers.get(type);
+ if (mp == null) {
+ mp = new MediaPlayer();
+ mMediaPlayers.put(type, mp);
+ } else {
+ mp.reset();
+ }
+ mp.setDataSource(audiblePath);
+ mp.prepare();
+ mp.setOnCompletionListener(mPlayCompletionListener);
+ return mp;
+ }
+
+ private MediaPlayer initAudibleMediaPlayer(AssetFileDescriptor afd, int type) throws IOException {
+ MediaPlayer mp = mMediaPlayers.get(type);
+ if (mp == null) {
+ mp = new MediaPlayer();
+ mMediaPlayers.put(type, mp);
+ } else {
+ mp.reset();
+ }
+ mp.setDataSource(afd.getFileDescriptor(),
+ afd.getStartOffset(), afd.getLength());
+ mp.prepare();
+ mp.setOnCompletionListener(mPlayCompletionListener);
+ return mp;
+ }
+
+ private void addAudibleToLayout(int type, MediaPlayer mp) {
+ View view = View.inflate(getActivity(), R.layout.audible_preview_item, null);
+ TextView tv = (TextView) view.findViewById(R.id.audible_name);
+ switch (type) {
+ case RingtoneManager.TYPE_ALARM:
+ tv.setText(R.string.alarm_label);
+ break;
+ case RingtoneManager.TYPE_NOTIFICATION:
+ tv.setText(R.string.notification_label);
+ break;
+ case RingtoneManager.TYPE_RINGTONE:
+ tv.setText(R.string.ringtone_label);
+ break;
+ }
+ ImageView iv = (ImageView) view.findViewById(R.id.btn_play_pause);
+ iv.setTag(mp);
+ iv.setOnClickListener(mPlayPauseClickListener);
+ mContent.addView(view, ITEM_LAYOUT_PARAMS);
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/BootAniPreviewFragment.java b/src/org/cyanogenmod/theme/chooser/BootAniPreviewFragment.java
new file mode 100644
index 0000000..da81ee8
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/BootAniPreviewFragment.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.chooser;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import org.cyanogenmod.theme.util.BootAnimationHelper;
+import org.cyanogenmod.theme.widget.PartAnimationDrawable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+
+public class BootAniPreviewFragment extends Fragment {
+ private static final String TAG = "ThemeChooser";
+ private static final String PKG_EXTRA = "pkg_extra";
+
+ private String mPkgName;
+ private ImageView mPreview;
+ private ProgressBar mLoadingProgress;
+ private TextView mNoPreviewTextView;
+ private boolean mPreviewLoaded = false;
+ private boolean mIsVisibileToUser = false;
+ private boolean mAnimationStarted = false;
+ private List<PartAnimationDrawable> mAnimationParts;
+ private int mCurrentAnimationPartIndex;
+ private PartAnimationDrawable mCurrentAnimationPart;
+ private Timer mTimer;
+ private Object mAnimationLock;
+
+ public static BootAniPreviewFragment newInstance(String pkgName) {
+ BootAniPreviewFragment fragment = new BootAniPreviewFragment();
+ Bundle args = new Bundle();
+ args.putString(PKG_EXTRA, pkgName);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mPkgName = getArguments().getString(PKG_EXTRA);
+ mAnimationLock = new Object();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_boot_animation_preview, container, false);
+ mPreview = (ImageView) view.findViewById(R.id.animated_preview);
+ mLoadingProgress = (ProgressBar) view.findViewById(R.id.loading_progress);
+ mNoPreviewTextView = (TextView) view.findViewById(R.id.no_preview);
+ return view;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ destroyAnimation();
+ mPreviewLoaded = false;
+ if (mTimer != null) mTimer.cancel();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ (new AnimationLoader(getActivity(), mPkgName)).execute();
+ }
+
+ @Override
+ public void setUserVisibleHint(boolean isVisibleToUser) {
+ super.setUserVisibleHint(isVisibleToUser);
+ mIsVisibileToUser = isVisibleToUser;
+ if (isVisibleToUser) {
+ if (mPreviewLoaded && !mAnimationStarted) {
+ startAnimation();
+ }
+ }
+ }
+
+ public void destroyAnimation() {
+ mPreview.setImageDrawable(null);
+ synchronized (mAnimationLock) {
+ if (mAnimationParts == null) return;
+ for (PartAnimationDrawable anim : mAnimationParts) {
+ final int numFrames = anim.getNumberOfFrames();
+ for (int i = 0; i < numFrames; i++) {
+ Drawable d = anim.getFrame(i);
+ if (d instanceof BitmapDrawable) {
+ ((BitmapDrawable) d).getBitmap().recycle();
+ }
+ }
+ }
+ mAnimationParts.clear();
+ }
+ mCurrentAnimationPart = null;
+ mCurrentAnimationPartIndex = 0;
+ mAnimationStarted = false;
+ }
+
+ private void startAnimation() {
+ if (mIsVisibileToUser) {
+ mTimer = new Timer();
+ long startTime = 100;
+ for (PartAnimationDrawable anim : mAnimationParts) {
+ if (anim.isOneShot()) {
+ for (int i = 0; i < anim.getPlayCount(); i++) {
+ mTimer.schedule(new AnimationUpdateTask(anim), startTime);
+ startTime += anim.getAnimationDuration();
+ }
+ } else {
+ mTimer.schedule(new AnimationUpdateTask(anim), startTime);
+ }
+ }
+ mAnimationStarted = true;
+ } else {
+ if (mAnimationParts != null && mAnimationParts.size() > 0)
+ mPreview.setImageDrawable(mAnimationParts.get(0).getFrame(0));
+ }
+ }
+
+ class AnimationLoader extends AsyncTask<Void, Void, Boolean> {
+ Context mContext;
+ String mPkgName;
+
+ public AnimationLoader(Context context, String pkgName) {
+ mContext = context;
+ mPkgName = pkgName;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ mPreview.setImageDrawable(null);
+ mLoadingProgress.setVisibility(View.VISIBLE);
+ mNoPreviewTextView.setVisibility(View.INVISIBLE);
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ if (mContext == null) {
+ return Boolean.FALSE;
+ }
+ InputStream is;
+ if ("default".equals(mPkgName)) {
+ try {
+ is = new ZipInputStream(
+ new FileInputStream(BootAnimationHelper.SYSTEM_BOOT_ANI_PATH));
+ } catch (FileNotFoundException e) {
+ return Boolean.FALSE;
+ }
+ } else {
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ ApplicationInfo ai = pm.getApplicationInfo(mPkgName, 0);
+ ZipFile zip = new ZipFile(new File(ai.sourceDir));
+ is = zip.getInputStream(zip.getEntry(
+ BootAnimationHelper.THEME_INTERNAL_BOOT_ANI_PATH));
+ } catch (PackageManager.NameNotFoundException e) {
+ return Boolean.FALSE;
+ } catch (ZipException e) {
+ return Boolean.FALSE;
+ } catch (IOException e) {
+ return Boolean.FALSE;
+ }
+ }
+ if (is != null) {
+ try {
+ synchronized (mAnimationLock) {
+ mAnimationParts = BootAnimationHelper.loadAnimation(mContext, is);
+ }
+ } catch (IOException e) {
+ return Boolean.FALSE;
+ }
+ }
+ return Boolean.TRUE;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean isSuccess) {
+ super.onPostExecute(isSuccess);
+ mLoadingProgress.setVisibility(View.INVISIBLE);
+ if (Boolean.TRUE.equals(isSuccess) && mAnimationParts != null) {
+ mPreviewLoaded = true;
+ startAnimation();
+ } else {
+ mNoPreviewTextView.setVisibility(View.VISIBLE);
+ Log.e(TAG, "Unable to load boot animation for preview.");
+ }
+ }
+ }
+
+ class AnimationUpdateTask extends TimerTask {
+ private PartAnimationDrawable mAnimation;
+ public AnimationUpdateTask(PartAnimationDrawable anim) {
+ mAnimation = anim;
+ }
+
+ @Override
+ public void run() {
+ mPreview.post(new Runnable() {
+ @Override
+ public void run() {
+ mPreview.setImageDrawable(mAnimation);
+ mAnimation.stop();
+ mAnimation.start();
+ }
+ });
+ }
+ }
+} \ No newline at end of file
diff --git a/src/org/cyanogenmod/theme/chooser/ChooserActivity.java b/src/org/cyanogenmod/theme/chooser/ChooserActivity.java
new file mode 100644
index 0000000..6fbf9ec
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/ChooserActivity.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.chooser;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import org.cyanogenmod.theme.util.NotificationHelper;
+
+public class ChooserActivity extends FragmentActivity {
+ public static final String TAG = ChooserActivity.class.getName();
+ public static final String EXTRA_COMPONENT_FILTER = "component_filter";
+ public static final String EXTRA_PKGNAME = "pkgName";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ NotificationHijackingService.ensureEnabled(this);
+
+ if (savedInstanceState == null) {
+ //Determine if there we need to filter by component (ex icon sets only)
+ Bundle extras = (Bundle) getIntent().getExtras();
+ String filter = (extras == null) ? null : extras.getString(EXTRA_COMPONENT_FILTER);
+
+ // If activity started by wallpaper chooser then filter on wallpapers
+ if (Intent.ACTION_SET_WALLPAPER.equals(getIntent().getAction())) {
+ filter = "mods_homescreen";
+ }
+
+ // Support filters passed in as csv. Since XML prefs do not support
+ // passing extras in as arrays.
+ ArrayList<String> filtersList = new ArrayList<String>();
+ if (filter != null) {
+ String[] filters = filter.split(",");
+ filtersList.addAll(Arrays.asList(filters));
+ }
+
+ Fragment fragment = null;
+ if (Intent.ACTION_MAIN.equals(getIntent().getAction()) &&
+ getIntent().hasExtra(EXTRA_PKGNAME)) {
+ // Handle case where Theme Store or some other app wishes to open
+ // a detailed theme view for a given package
+ // TODO: Handle if a bad pkg is provided
+ String pkgName = getIntent().getStringExtra(EXTRA_PKGNAME);
+ fragment = ChooserDetailFragment.newInstance(pkgName, null);
+ } else {
+ fragment = ChooserBrowseFragment.newInstance(filtersList);
+ }
+ getSupportFragmentManager().beginTransaction().replace(R.id.content, fragment, "ChooserBrowseFragment").commit();
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/ChooserBrowseFragment.java b/src/org/cyanogenmod/theme/chooser/ChooserBrowseFragment.java
new file mode 100644
index 0000000..ddfa3d1
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/ChooserBrowseFragment.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.chooser;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.cyanogenmod.theme.chooser.WallpaperAndIconPreviewFragment.IconInfo;
+import org.cyanogenmod.theme.util.BootAnimationHelper;
+import org.cyanogenmod.theme.util.IconPreviewHelper;
+import org.cyanogenmod.theme.util.ThemedTypefaceHelper;
+import org.cyanogenmod.theme.util.Utils;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.ThemesContract.ThemesColumns;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.widget.CursorAdapter;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.webkit.URLUtil;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class ChooserBrowseFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
+ public ListView mListView;
+ public LocalPagerAdapter mAdapter;
+ public ArrayList<String> mComponentFilters;
+
+ private Point mMaxImageSize = new Point(); //Size of preview image in listview
+
+ public static ChooserBrowseFragment newInstance(ArrayList<String> componentFilters) {
+ ChooserBrowseFragment fragment = new ChooserBrowseFragment();
+ Bundle args = new Bundle();
+ args.putStringArrayList(ChooserActivity.EXTRA_COMPONENT_FILTER, componentFilters);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_chooser_browse, container, false);
+ ArrayList<String> filters = getArguments().getStringArrayList(ChooserActivity.EXTRA_COMPONENT_FILTER);
+ mComponentFilters = (filters != null) ? filters : new ArrayList<String>(0);
+ mListView = (ListView) v.findViewById(R.id.list);
+ mAdapter = new LocalPagerAdapter(getActivity(), null, mComponentFilters);
+ mListView.setAdapter(mAdapter);
+ mListView.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ String pkgName = (String) mAdapter.getItem(position);
+ ChooserDetailFragment fragment = ChooserDetailFragment.newInstance(pkgName, mComponentFilters);
+ FragmentTransaction transaction = getFragmentManager().beginTransaction();
+ transaction.replace(R.id.content, fragment, ChooserDetailFragment.class.toString());
+ transaction.addToBackStack(null);
+ transaction.commit();
+ }
+ });
+ getLoaderManager().initLoader(0, null, this);
+
+ Display display = getActivity().getWindowManager().getDefaultDisplay();
+ display.getSize(mMaxImageSize);
+ mMaxImageSize.y = (int) getActivity().getResources().getDimension(R.dimen.item_browse_height);
+
+ return v;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ mAdapter.swapCursor(data);
+ mAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ mAdapter.swapCursor(null);
+ mAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ String selection;
+ String selectionArgs[] = null;
+ if (mComponentFilters.isEmpty()) {
+ selection = ThemesColumns.PRESENT_AS_THEME + "=?";
+ selectionArgs = new String[] {"1"};
+ } else {
+ StringBuffer sb = new StringBuffer();
+ for(int i=0; i < mComponentFilters.size(); i++) {
+ sb.append(mComponentFilters.get(i));
+ sb.append("=1");
+ if (i != mComponentFilters.size()-1) {
+ sb.append(" OR ");
+ }
+ }
+ selection = sb.toString();
+ }
+
+ return new CursorLoader(getActivity(), ThemesColumns.CONTENT_URI, null, selection,
+ selectionArgs, "(" + ThemesColumns.PKG_NAME + "='default') DESC, "
+ + ThemesColumns.TITLE + " ASC");
+ }
+
+ public class LocalPagerAdapter extends CursorAdapter {
+ List<String> mFilters;
+
+ public LocalPagerAdapter(Context context, Cursor c, List<String> filters) {
+ super(context, c, 0);
+ mFilters = filters;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ mCursor.moveToPosition(position);
+ int pkgIdx = mCursor.getColumnIndex(ThemesColumns.PKG_NAME);
+ String pkgName = (String) mCursor.getString(pkgIdx);
+ return pkgName;
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ int titleIdx = mCursor.getColumnIndex(ThemesColumns.TITLE);
+ int authorIdx = mCursor.getColumnIndex(ThemesColumns.AUTHOR);
+ int hsIdx = mCursor.getColumnIndex(ThemesColumns.HOMESCREEN_URI);
+ int wpIdx = mCursor.getColumnIndex(ThemesColumns.WALLPAPER_URI);
+ int styleIdx = mCursor.getColumnIndex(ThemesColumns.STYLE_URI);
+ int pkgIdx = mCursor.getColumnIndex(ThemesColumns.PKG_NAME);
+ int legacyIndex = mCursor.getColumnIndex(ThemesColumns.IS_LEGACY_THEME);
+
+ String title = mCursor.getString(titleIdx);
+ String author = mCursor.getString(authorIdx);
+ String pkgName = mCursor.getString(pkgIdx);
+ String hsImagePath = "default".equals(pkgName) ? mCursor.getString(hsIdx) :
+ mCursor.getString(wpIdx);
+ String styleImagePath = mCursor.getString(styleIdx);
+ boolean isLegacyTheme = mCursor.getInt(legacyIndex) == 1;
+
+ ThemeItemHolder item = (ThemeItemHolder) view.getTag();
+ item.title.setText(title);
+ item.author.setText(author);
+ if (mFilters.isEmpty()) {
+ bindDefaultView(item, pkgName, hsImagePath, isLegacyTheme);
+ } else if (mFilters.contains(ThemesColumns.MODIFIES_BOOT_ANIM)) {
+ bindBootAnimView(item, context, pkgName);
+ } else if (mFilters.contains(ThemesColumns.MODIFIES_LAUNCHER)) {
+ bindWallpaperView(item, pkgName, hsImagePath, isLegacyTheme);
+ } else if (mFilters.contains(ThemesColumns.MODIFIES_FONTS)) {
+ bindFontView(view, context, pkgName);
+ } else if (mFilters.contains(ThemesColumns.MODIFIES_OVERLAYS)) {
+ bindOverlayView(item, pkgName, styleImagePath, isLegacyTheme);
+ } else if (mFilters.contains(ThemesColumns.MODIFIES_ICONS)) {
+ bindDefaultView(item, pkgName, hsImagePath, isLegacyTheme);
+ bindIconView(view, context, pkgName);
+ } else {
+ bindDefaultView(item, pkgName, hsImagePath, isLegacyTheme);
+ }
+ }
+
+ private void bindDefaultView(ThemeItemHolder item, String pkgName,
+ String hsImagePath, boolean isLegacyTheme) {
+ //Do not load wallpaper if we preview icons
+ if (mFilters.contains(ThemesColumns.MODIFIES_ICONS)) return;
+
+ if (isLegacyTheme) {
+ item.thumbnail.setTag(pkgName);
+ } else {
+ item.thumbnail.setTag(hsImagePath);
+ }
+ item.thumbnail.setImageDrawable(null);
+
+ if (item.thumbnail.getTag() != null) {
+ LoadImage loadImageTask = new LoadImage(item.thumbnail, isLegacyTheme, false, pkgName);
+ loadImageTask.execute();
+ }
+ }
+
+ private void bindOverlayView(ThemeItemHolder item, String pkgName,
+ String styleImgPath, boolean isLegacyTheme) {
+ if (isLegacyTheme) {
+ item.thumbnail.setTag(pkgName);
+ } else {
+ item.thumbnail.setTag(styleImgPath);
+ }
+ item.thumbnail.setImageDrawable(null);
+
+ //Crop the bottom
+ if (!isLegacyTheme) {
+ item.thumbnail.setScaleType(ScaleType.MATRIX);
+ }
+
+ if (item.thumbnail.getTag() != null) {
+ LoadImage loadImageTask = new LoadImage(item.thumbnail, isLegacyTheme, false, pkgName);
+ loadImageTask.execute();
+ }
+ }
+
+ private void bindBootAnimView(ThemeItemHolder item, Context context, String pkgName) {
+ (new BootAnimationHelper.LoadBootAnimationImage(item.thumbnail, context, pkgName)).execute();
+ }
+
+ private void bindWallpaperView(ThemeItemHolder item, String pkgName,
+ String hsImagePath, boolean isLegacyTheme) {
+ if (isLegacyTheme) {
+ item.thumbnail.setTag(pkgName);
+ } else {
+ item.thumbnail.setTag(hsImagePath);
+ }
+ item.thumbnail.setImageDrawable(null);
+
+ if (item.thumbnail.getTag() != null) {
+ LoadImage loadImageTask = new LoadImage(item.thumbnail, isLegacyTheme, true, pkgName);
+ loadImageTask.execute();
+ }
+ }
+
+ public void bindFontView(View view, Context context, String pkgName) {
+ FontItemHolder item = (FontItemHolder) view.getTag();
+ ThemedTypefaceHelper helper = new ThemedTypefaceHelper();
+ helper.load(mContext, pkgName);
+ Typeface typefaceNormal = helper.getTypeface(Typeface.NORMAL);
+ Typeface typefaceBold = helper.getTypeface(Typeface.BOLD);
+ item.textView.setTypeface(typefaceNormal);
+ item.textViewBold.setTypeface(typefaceBold);
+ }
+
+ public void bindIconView(View view, Context context, String pkgName) {
+ ThemeItemHolder holder = (ThemeItemHolder) view.getTag();
+ LoadIconsTask loadImageTask = new LoadIconsTask(context, pkgName, holder.mIconHolders);
+ loadImageTask.execute();
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ if (mComponentFilters.isEmpty()) {
+ return newDefaultView(context, cursor, parent);
+ } else if (mComponentFilters.contains(ThemesColumns.MODIFIES_FONTS)) {
+ return newFontView(context, cursor, parent);
+ } else if (mComponentFilters.contains(ThemesColumns.MODIFIES_ICONS)) {
+ return newDefaultView(context, cursor, parent);
+ }
+ return newDefaultView(context, cursor, parent);
+ }
+
+ private View newDefaultView(Context context, Cursor cursor, ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ View row = inflater.inflate(R.layout.item_store_browse, parent, false);
+ ThemeItemHolder item = new ThemeItemHolder();
+ item.thumbnail = (ImageView) row.findViewById(R.id.image);
+ item.title = (TextView) row.findViewById(R.id.title);
+ item.author = (TextView) row.findViewById(R.id.author);
+ item.mIconHolders = (ViewGroup) row.findViewById(R.id.icon_container);
+ row.setTag(item);
+ return row;
+ }
+
+ private View newFontView(Context context, Cursor cursor, ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ View row = inflater.inflate(R.layout.item_chooser_browse_font, parent, false);
+ FontItemHolder item = new FontItemHolder();
+ item.textView = (TextView) row.findViewById(R.id.text1);
+ item.textViewBold = (TextView) row.findViewById(R.id.text2);
+ item.title = (TextView) row.findViewById(R.id.title);
+ item.author = (TextView) row.findViewById(R.id.author);
+ row.setTag(item);
+ return row;
+ }
+ }
+
+ public static class ThemeItemHolder {
+ ImageView thumbnail;
+ TextView title;
+ TextView author;
+ ViewGroup mIconHolders;
+ }
+
+ public static class FontItemHolder extends ThemeItemHolder {
+ TextView textView;
+ TextView textViewBold;
+ }
+
+ public class LoadImage extends AsyncTask<Object, Void, Bitmap> {
+ private ImageView imv;
+ private String path;
+ private boolean isLegacyTheme;
+ private boolean showWallpaper;
+ private String pkgName;
+
+ public LoadImage(ImageView imv, boolean isLegacyTheme, boolean showWallpaper, String pkgName) {
+ this.imv = imv;
+ this.path = imv.getTag().toString();
+ this.isLegacyTheme = isLegacyTheme;
+ this.showWallpaper = showWallpaper;
+ this.pkgName = pkgName;
+ }
+
+ @Override
+ protected Bitmap doInBackground(Object... params) {
+ Bitmap bitmap = null;
+ if (!isLegacyTheme) {
+ if ("default".equals(pkgName)) {
+ Resources res = getActivity().getResources();
+ AssetManager assets = new AssetManager();
+ assets.addAssetPath(WallpaperAndIconPreviewFragment.FRAMEWORK_RES);
+ Resources frameworkRes = new Resources(assets, res.getDisplayMetrics(),
+ res.getConfiguration());
+ bitmap = Utils.decodeResource(frameworkRes,
+ com.android.internal.R.drawable.default_wallpaper,
+ mMaxImageSize.x, mMaxImageSize.y);
+ } else {
+ if (URLUtil.isAssetUrl(path)) {
+ Context ctx = getActivity();
+ try {
+ ctx = getActivity().createPackageContext(pkgName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+
+ }
+ bitmap = Utils.getBitmapFromAsset(ctx, path, mMaxImageSize.x, mMaxImageSize.y);
+ } else if (path != null) {
+ bitmap = Utils.decodeFile(path, mMaxImageSize.x, mMaxImageSize.y);
+ }
+ }
+ } else {
+ try {
+ PackageManager pm = getActivity().getPackageManager();
+ PackageInfo pi = pm.getPackageInfo(path, 0);
+ final Context themeContext = getActivity().createPackageContext(path,
+ Context.CONTEXT_IGNORE_SECURITY);
+ final Resources res = themeContext.getResources();
+ final int resId = showWallpaper ? pi.legacyThemeInfos[0].wallpaperResourceId :
+ pi.legacyThemeInfos[0].previewResourceId;
+ bitmap = Utils.decodeResource(res, resId, mMaxImageSize.x, mMaxImageSize.y);
+ } catch (PackageManager.NameNotFoundException e) {
+ bitmap = null;
+ }
+ }
+ return bitmap;
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap result) {
+ if (!imv.getTag().toString().equals(path)) {
+ return;
+ }
+
+ if (result != null && imv != null) {
+ imv.setVisibility(View.VISIBLE);
+ imv.setImageBitmap(result);
+ }
+ }
+ }
+
+
+ public static class LoadIconsTask extends AsyncTask<Void, Void, List<IconInfo>> {
+ private String mPkgName;
+ private Context mContext;
+ private ViewGroup mIconViewGroup;
+
+ public LoadIconsTask(Context context, String pkgName, ViewGroup iconViewGroup) {
+ mPkgName = pkgName;
+ mContext = context.getApplicationContext();
+ mIconViewGroup = iconViewGroup;
+ mIconViewGroup.setTag(pkgName);
+ }
+
+ @Override
+ protected List<IconInfo> doInBackground(Void... arg0) {
+ List<IconInfo> icons = new ArrayList<IconInfo>();
+ IconPreviewHelper helper = new IconPreviewHelper(mContext, mPkgName);
+
+ for (ComponentName component : WallpaperAndIconPreviewFragment.ICON_COMPONENTS) {
+ Drawable icon = helper.getIcon(component);
+ IconInfo info = new IconInfo(null, icon);
+ icons.add(info);
+ }
+
+ return icons;
+ }
+
+ @Override
+ protected void onPostExecute(List<IconInfo> icons) {
+ if (!mIconViewGroup.getTag().toString().equals(mPkgName) || icons == null) {
+ return;
+ }
+
+ mIconViewGroup.removeAllViews();
+ for (IconInfo info : icons) {
+ LinearLayout.LayoutParams lparams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ ImageView imageView = new ImageView(mContext);
+ int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ 8, mContext.getResources().getDisplayMetrics());
+ imageView.setPadding(padding, 0, padding, 0);
+ imageView.setLayoutParams(lparams);
+ imageView.setScaleType(ImageView.ScaleType.CENTER);
+ imageView.setImageDrawable(info.icon);
+ mIconViewGroup.addView(imageView);
+ }
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/ChooserDetailFragment.java b/src/org/cyanogenmod/theme/chooser/ChooserDetailFragment.java
new file mode 100644
index 0000000..622abab
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/ChooserDetailFragment.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.chooser;
+
+import android.content.Context;
+import android.content.res.ThemeManager;
+import android.content.res.ThemeManager.ThemeChangeListener;
+import android.database.Cursor;
+import android.graphics.drawable.ClipDrawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.ThemesContract;
+import android.provider.ThemesContract.MixnMatchColumns;
+import android.provider.ThemesContract.ThemesColumns;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.view.ViewPager;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.TextView;
+import com.sothree.slidinguppanel.SlidingupPanelLayout;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public class ChooserDetailFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>, ThemeChangeListener {
+ public static final HashMap<String, Integer> sComponentToId = new HashMap<String, Integer>();
+
+ private static final String TAG = ChooserDetailFragment.class.getName();
+ private static final int LOADER_ID_THEME_INFO = 0;
+ private static final int LOADER_ID_APPLIED_THEME = 1;
+
+ private TextView mTitle;
+ private TextView mAuthor;
+ private Button mApply;
+ private ViewPager mPager;
+ private ThemeDetailPagerAdapter mPagerAdapter;
+ private String mPkgName;
+ private SlidingupPanelLayout mSlidingPanel;
+
+ private Handler mHandler;
+ private Cursor mAppliedThemeCursor;
+ private HashMap<String, CheckBox> mComponentToCheckbox = new HashMap<String, CheckBox>();
+
+ private boolean mLoadInitialCheckboxStates = true;
+ private SparseArray<Boolean> mInitialCheckboxStates = new SparseArray<Boolean>();
+ private SparseArray<Boolean> mCurrentCheckboxStates = new SparseArray<Boolean>();
+
+ // allows emphasis on a particular aspect of a theme. ex "mods_icons" would
+ // uncheck all components but icons and sets the first preview image to be the icon pack
+ private ArrayList<String> mComponentFilters;
+
+ private ThemeManager mService;
+
+ static {
+ sComponentToId.put(ThemesColumns.MODIFIES_OVERLAYS, R.id.chk_overlays);
+ sComponentToId.put(ThemesColumns.MODIFIES_BOOT_ANIM, R.id.chk_boot_anims);
+ sComponentToId.put(ThemesColumns.MODIFIES_FONTS, R.id.chk_fonts);
+ sComponentToId.put(ThemesColumns.MODIFIES_ICONS, R.id.chk_icons);
+ sComponentToId.put(ThemesColumns.MODIFIES_LAUNCHER, R.id.chk_wallpaper);
+ sComponentToId.put(ThemesColumns.MODIFIES_LOCKSCREEN, R.id.chk_lockscreen);
+ sComponentToId.put(ThemesColumns.MODIFIES_RINGTONES, R.id.chk_ringtones);
+ sComponentToId.put(ThemesColumns.MODIFIES_NOTIFICATIONS, R.id.chk_notifications);
+ sComponentToId.put(ThemesColumns.MODIFIES_ALARMS, R.id.chk_alarms);
+ }
+
+ public static ChooserDetailFragment newInstance(String pkgName, ArrayList<String> componentFilters) {
+ ChooserDetailFragment fragment = new ChooserDetailFragment();
+ Bundle args = new Bundle();
+ args.putString("pkgName", pkgName);
+ args.putStringArrayList(ChooserActivity.EXTRA_COMPONENT_FILTER, componentFilters);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mHandler = new Handler();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ mPkgName = getArguments().getString("pkgName");
+ ArrayList<String> filters = getArguments().getStringArrayList(ChooserActivity.EXTRA_COMPONENT_FILTER);
+ mComponentFilters = (filters != null) ? filters : new ArrayList<String>(0);
+ View v = inflater.inflate(R.layout.fragment_chooser_theme_pager_item, container, false);
+ mTitle = (TextView) v.findViewById(R.id.title);
+ mAuthor = (TextView) v.findViewById(R.id.author);
+ mPager = (ViewPager) v.findViewById(R.id.pager);
+ mPagerAdapter = new ThemeDetailPagerAdapter(getChildFragmentManager());
+ mPager.setAdapter(mPagerAdapter);
+ mApply = (Button) v.findViewById(R.id.apply);
+
+ mApply.setOnClickListener(new OnClickListener() {
+ public void onClick(View view) {
+ List<String> components = getCheckedComponents();
+ mService.requestThemeChange(mPkgName, components);
+ mApply.setText(R.string.applying);
+ }
+ });
+
+ mSlidingPanel = (SlidingupPanelLayout) v.findViewById(R.id.sliding_layout);
+
+ // Find all the checkboxes for theme components (ex wallpaper)
+ for (Map.Entry<String, Integer> entry : sComponentToId.entrySet()) {
+ CheckBox componentCheckbox = (CheckBox) v.findViewById(entry.getValue());
+ mComponentToCheckbox.put(entry.getKey(), componentCheckbox);
+ componentCheckbox.setOnCheckedChangeListener(mComponentCheckChangedListener);
+ }
+
+ getLoaderManager().initLoader(LOADER_ID_THEME_INFO, null, this);
+ getLoaderManager().initLoader(LOADER_ID_APPLIED_THEME, null, this);
+ mService = (ThemeManager) getActivity().getSystemService(Context.THEME_SERVICE);
+ return v;
+ }
+
+ private List<String> getCheckedComponents() {
+ // Get all checked components
+ List<String> components = new ArrayList<String>();
+ for (Map.Entry<String, CheckBox> entry : mComponentToCheckbox.entrySet()) {
+ String component = entry.getKey();
+ CheckBox checkbox = entry.getValue();
+ if (checkbox.isChecked()) {
+ components.add(component);
+ }
+ }
+ return components;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mService != null) {
+ mService.onClientResumed(mPkgName, this);
+ }
+ refreshApplyButton();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (mService != null) {
+ mService.onClientPaused(mPkgName);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mService != null) {
+ mService.onClientDestroyed(mPkgName);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mSlidingPanel.post(mShowSlidingPanelRunnable);
+ }
+
+ private Runnable mShowSlidingPanelRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mSlidingPanel.expandPane(mSlidingPanel.getAnchorPoint());
+ }
+ };
+
+ private OnCheckedChangeListener mComponentCheckChangedListener = new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ mCurrentCheckboxStates.put(buttonView.getId(), isChecked);
+ if (componentSelectionChanged()) {
+ mApply.setEnabled(true);
+ } else {
+ mApply.setEnabled(false);
+ }
+ }
+ };
+
+ private boolean componentSelectionChanged() {
+ if (mCurrentCheckboxStates.size() != mInitialCheckboxStates.size()) return false;
+
+ int N = mInitialCheckboxStates.size();
+ for (int i = 0; i < N; i++) {
+ int key = mInitialCheckboxStates.keyAt(i);
+ if (!mInitialCheckboxStates.get(key).equals(mCurrentCheckboxStates.get(key))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ Uri uri = null;
+ String selection = null;
+ String[] selectionArgs = null;
+
+ switch (id) {
+ case LOADER_ID_THEME_INFO:
+ uri = ThemesColumns.CONTENT_URI;
+ selection = ThemesColumns.PKG_NAME + "= ?";
+ selectionArgs = new String[] { mPkgName };
+ break;
+ case LOADER_ID_APPLIED_THEME:
+ uri = MixnMatchColumns.CONTENT_URI;
+ break;
+ }
+
+ return new CursorLoader(getActivity(), uri, null, selection, selectionArgs, null);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
+ int id = cursorLoader.getId();
+
+ if (id == LOADER_ID_THEME_INFO) {
+ if (cursor.getCount() == 0) {
+ //Theme was deleted
+ safelyPopStack();
+ } else {
+ loadThemeInfo(cursor);
+ }
+ } else if (id == LOADER_ID_APPLIED_THEME) {
+ loadAppliedInfo(cursor);
+ }
+ }
+
+ /**
+ * Avoid IllegalStateException when popping the backstack
+ * in onLoadFinished.
+ */
+ private void safelyPopStack() {
+ Runnable r = new Runnable() {
+ public void run() {
+ getFragmentManager().popBackStackImmediate();
+ }
+ };
+ mHandler.post(r);
+ }
+
+ private void loadThemeInfo(Cursor cursor) {
+ cursor.moveToFirst();
+ int titleIdx = cursor.getColumnIndex(ThemesColumns.TITLE);
+ int authorIdx = cursor.getColumnIndex(ThemesColumns.AUTHOR);
+ int hsIdx = cursor.getColumnIndex(ThemesColumns.HOMESCREEN_URI);
+ int legacyIdx = cursor.getColumnIndex(ThemesColumns.IS_LEGACY_THEME);
+ int styleIdx = cursor.getColumnIndex(ThemesColumns.STYLE_URI);
+ boolean isLegacyTheme = cursor.getInt(legacyIdx) == 1;
+ String title = cursor.getString(titleIdx);
+ String author = cursor.getString(authorIdx);
+ String hsImagePath = isLegacyTheme ? mPkgName : cursor.getString(hsIdx);
+ String styleImagePath = cursor.getString(styleIdx);
+
+ mTitle.setText(title);
+ mAuthor.setText(author);
+
+ // Configure checkboxes for all the theme components
+ List<String> supportedComponents = new LinkedList<String>();
+ for (Map.Entry<String, CheckBox> entry : mComponentToCheckbox.entrySet()) {
+ String componentName = entry.getKey();
+ CheckBox componentCheckbox = entry.getValue();
+ int idx = cursor.getColumnIndex(componentName);
+ boolean componentIncludedInTheme = cursor.getInt(idx) == 1;
+
+
+ if (!shouldComponentBeVisible(componentName)) {
+ componentCheckbox.setVisibility(View.GONE);
+ }
+
+ if (shouldComponentBeEnabled(componentName, componentIncludedInTheme)) {
+ componentCheckbox.setEnabled(true);
+ } else {
+ componentCheckbox.setEnabled(false);
+ }
+
+ if (componentIncludedInTheme) {
+ supportedComponents.add(componentName);
+ }
+ }
+
+ mPagerAdapter.setPreviewImage(hsImagePath, isLegacyTheme);
+ mPagerAdapter.setStyleImage(styleImagePath);
+ mPagerAdapter.update(supportedComponents);
+ }
+
+ private boolean shouldComponentBeVisible(String componentName) {
+ // Theme pack, so it is always visible
+ if (mComponentFilters.isEmpty()) return true;
+ //Not in a theme pack
+ return !componentFiltered(componentName);
+ }
+
+ private boolean shouldComponentBeEnabled(String componentName, boolean componentIncludedInTheme) {
+ return !componentFiltered(componentName) && componentIncludedInTheme;
+ }
+
+
+ private boolean componentFiltered(String componentName) {
+ if (mComponentFilters.isEmpty()) return false;
+ return !mComponentFilters.contains(componentName);
+ }
+
+ private void loadAppliedInfo(Cursor cursor) {
+ mAppliedThemeCursor = cursor;
+ refreshChecksForCheckboxes();
+ refreshApplyButton();
+ }
+
+ private void refreshChecksForCheckboxes() {
+
+ //Determine which components are applied
+ List<String> appliedComponents = new ArrayList<String>();
+ if (mAppliedThemeCursor != null) {
+ mAppliedThemeCursor.moveToPosition(-1);
+ while (mAppliedThemeCursor.moveToNext()) {
+ String mixnmatchkey = mAppliedThemeCursor.getString(mAppliedThemeCursor.getColumnIndex(MixnMatchColumns.COL_KEY));
+ String component = ThemesContract.MixnMatchColumns.mixNMatchKeyToComponent(mixnmatchkey);
+ String pkg = mAppliedThemeCursor.getString(mAppliedThemeCursor.getColumnIndex(MixnMatchColumns.COL_VALUE));
+
+ if (pkg.equals(mPkgName)) {
+ appliedComponents.add(component);
+ }
+ }
+ }
+
+ //Apply checks
+ for (Map.Entry<String, CheckBox> entry : mComponentToCheckbox.entrySet()) {
+ String componentName = entry.getKey();
+ CheckBox componentCheckbox = entry.getValue();
+
+ if (appliedComponents.contains(componentName)) {
+ componentCheckbox.setChecked(true);
+ }
+ if (mLoadInitialCheckboxStates) {
+ mInitialCheckboxStates.put(componentCheckbox.getId(),
+ componentCheckbox.isChecked());
+ }
+ mCurrentCheckboxStates.put(componentCheckbox.getId(), componentCheckbox.isChecked());
+ }
+ }
+
+ private void refreshApplyButton() {
+ //Default
+ mApply.setText(R.string.apply);
+ StateListDrawable d = (StateListDrawable) mApply.getBackground();
+ LayerDrawable bg = (LayerDrawable) d.getStateDrawable(
+ d.getStateDrawableIndex(new int[] {android.R.attr.state_enabled}));
+ final ClipDrawable clip = (ClipDrawable) bg.findDrawableByLayerId(android.R.id.progress);
+ clip.setLevel(0);
+
+ //Determine whether the apply button should show "apply" or "update"
+ if (mAppliedThemeCursor != null) {
+ mAppliedThemeCursor.moveToPosition(-1);
+ while (mAppliedThemeCursor.moveToNext()) {
+ String component = mAppliedThemeCursor.getString(mAppliedThemeCursor.getColumnIndex(MixnMatchColumns.COL_KEY));
+ String pkg = mAppliedThemeCursor.getString(mAppliedThemeCursor.getColumnIndex(MixnMatchColumns.COL_VALUE));
+
+ // At least one component is set here for this theme
+ if (pkg != null && mPkgName.equals(pkg)) {
+ mApply.setText(R.string.update);
+ break;
+ }
+ }
+ }
+
+ //Determine if the apply button's progress
+ int progress = (mService == null) ? 0 : mService.getProgress(mPkgName);
+ if (progress != 0) {
+ clip.setLevel(progress * 100);
+ mApply.setText(R.string.applying);
+ mApply.setClickable(false);
+ } else {
+ mApply.setClickable(true);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> cursor) {
+ mAppliedThemeCursor = null;
+ }
+
+ @Override
+ public void onProgress(int progress) {
+ refreshApplyButton();
+ }
+
+ @Override
+ public void onFinish(boolean isSuccess) {
+ Log.d(TAG, "Finished Applying Theme success=" + isSuccess);
+ refreshApplyButton();
+ }
+
+ public class ThemeDetailPagerAdapter extends FragmentStatePagerAdapter {
+ private List<String> mPreviewList = new LinkedList<String>();
+ private List<String> mSupportedComponents = Collections.emptyList();
+ private String mPreviewImagePath;
+ private boolean mIsLegacyTheme;
+ private String mStyleImagePath;
+
+ public ThemeDetailPagerAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ public void setPreviewImage(String imagePath, boolean isLegacyTheme) {
+ mPreviewImagePath = imagePath;
+ mIsLegacyTheme = isLegacyTheme;
+ }
+
+ public void setStyleImage(String imagePath) {
+ mStyleImagePath = imagePath;
+ }
+
+ private void update(List<String> supportedComponents) {
+ mSupportedComponents = supportedComponents;
+ mPreviewList.clear();
+ mPreviewList.addAll(supportedComponents);
+
+ // If a particular component is being emphasized
+ // then the preview image should reflect that by showing it first
+ for (String component : mComponentFilters) {
+ mPreviewList.remove(component);
+ mPreviewList.add(0, component);
+ }
+
+ // Wallpaper and Icons are previewed together so two fragments are not needed
+ if (mSupportedComponents.contains(ThemesColumns.MODIFIES_LAUNCHER) &&
+ mSupportedComponents.contains(ThemesColumns.MODIFIES_ICONS)) {
+ mPreviewList.remove(ThemesColumns.MODIFIES_ICONS);
+ }
+
+ //TODO: We don't have previewing for all the components yet. Remove these lines when we do.
+ mPreviewList.remove(ThemesColumns.MODIFIES_LOCKSCREEN);
+
+ // The AudiblePreviewFragment will take care of loading all available
+ // audibles so remove all but one so only one fragment instance is created
+ if (mSupportedComponents.contains(ThemesColumns.MODIFIES_ALARMS)) {
+ mPreviewList.remove(ThemesColumns.MODIFIES_NOTIFICATIONS);
+ mPreviewList.remove(ThemesColumns.MODIFIES_RINGTONES);
+ } else if (mSupportedComponents.contains(ThemesColumns.MODIFIES_NOTIFICATIONS)) {
+ mPreviewList.remove(ThemesColumns.MODIFIES_RINGTONES);
+ }
+
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return mPreviewList.size();
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ String component = mPreviewList.get(position);
+ Fragment fragment = null;
+
+ if (component.equals(ThemesColumns.MODIFIES_LAUNCHER)) {
+ boolean showIcons = mSupportedComponents.contains(ThemesColumns.MODIFIES_ICONS);
+ fragment = WallpaperAndIconPreviewFragment.newInstance(mPreviewImagePath, mPkgName, mIsLegacyTheme, showIcons);
+ } else if(component.equals(ThemesColumns.MODIFIES_OVERLAYS)) {
+ fragment = WallpaperAndIconPreviewFragment.newInstance(mStyleImagePath, mPkgName, mIsLegacyTheme, false);
+ } else if (component.equals(ThemesColumns.MODIFIES_BOOT_ANIM)) {
+ fragment = BootAniPreviewFragment.newInstance(mPkgName);
+ } else if (component.equals(ThemesColumns.MODIFIES_FONTS)) {
+ fragment = FontPreviewFragment.newInstance(mPkgName);
+ } else if (component.equals(ThemesColumns.MODIFIES_LOCKSCREEN)) {
+ throw new UnsupportedOperationException("Not implemented yet!");
+ } else if (component.equals(ThemesColumns.MODIFIES_LAUNCHER)) {
+ throw new UnsupportedOperationException("Not implemented yet!");
+ } else if (component.equals(ThemesColumns.MODIFIES_ICONS)) {
+ fragment = WallpaperAndIconPreviewFragment.newInstance(mPreviewImagePath, mPkgName, mIsLegacyTheme, mSupportedComponents.contains(ThemesColumns.MODIFIES_ICONS));
+ } else if (component.equals(ThemesColumns.MODIFIES_ALARMS)
+ || component.equals(ThemesColumns.MODIFIES_NOTIFICATIONS)
+ || component.equals(ThemesColumns.MODIFIES_RINGTONES)) {
+ fragment = AudiblePreviewFragment.newInstance(mPkgName);
+ } else {
+ throw new UnsupportedOperationException("Cannot preview " + component);
+ }
+ return fragment;
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/FontPreviewFragment.java b/src/org/cyanogenmod/theme/chooser/FontPreviewFragment.java
new file mode 100644
index 0000000..fdde292
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/FontPreviewFragment.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.chooser;
+
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.cyanogenmod.theme.util.ThemedTypefaceHelper;
+
+
+public class FontPreviewFragment extends Fragment {
+ private static final String PKG_EXTRA = "pkg_extra";
+ private String mPkgName;
+
+ private Typeface mTypefaceNormal;
+ private Typeface mTypefaceBold;
+ private Typeface mTypefaceItalic;
+ private Typeface mTypefaceBoldItalic;
+
+ private TextView mTv1;
+ private TextView mTv2;
+ private TextView mTv3;
+ private TextView mTv4;
+
+ static FontPreviewFragment newInstance(String pkgName) {
+ final FontPreviewFragment f = new FontPreviewFragment();
+ final Bundle args = new Bundle();
+ args.putString(PKG_EXTRA, pkgName);
+ f.setArguments(args);
+ return f;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mPkgName = getArguments().getString(PKG_EXTRA);
+
+ ThemedTypefaceHelper helper = new ThemedTypefaceHelper();
+ helper.load(getActivity(), mPkgName);
+ mTypefaceNormal = helper.getTypeface(Typeface.NORMAL);
+ mTypefaceBold = helper.getTypeface(Typeface.BOLD);
+ mTypefaceItalic = helper.getTypeface(Typeface.ITALIC);
+ mTypefaceBoldItalic = helper.getTypeface(Typeface.BOLD_ITALIC);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.font_preview_item, container, false);
+ mTv1 = (TextView) view.findViewById(R.id.text1);
+ mTv2 = (TextView) view.findViewById(R.id.text2);
+ mTv3 = (TextView) view.findViewById(R.id.text3);
+ mTv4 = (TextView) view.findViewById(R.id.text4);
+
+ mTv1.setTypeface(mTypefaceNormal);
+ mTv3.setTypeface(mTypefaceItalic);
+ mTv2.setTypeface(mTypefaceBold);
+ mTv4.setTypeface(mTypefaceBoldItalic);
+
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/NotificationHijackingService.java b/src/org/cyanogenmod/theme/chooser/NotificationHijackingService.java
new file mode 100644
index 0000000..41c3a46
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/NotificationHijackingService.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.chooser;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.provider.Settings;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
+
+public class NotificationHijackingService extends NotificationListenerService {
+ private static final String TAG = NotificationHijackingService.class.getName();
+ private static final String GOOGLE_PLAY_PACKAGE_NAME = "com.android.vending";
+ private static final String ACTION_INSTALLED =
+ "com.android.vending.SUCCESSFULLY_INSTALLED_CLICKED";
+ private static final String EXTRA_PACKAGE_NAME = "package_name";
+
+ @Override
+ public void onNotificationPosted(StatusBarNotification sbn) {
+ if (GOOGLE_PLAY_PACKAGE_NAME.equals(sbn.getPackageName())) {
+ PendingIntent contentIntent = sbn.getNotification().contentIntent;
+ if (contentIntent == null) return;
+ Intent intent = contentIntent.getIntent();
+ if (intent == null) return;
+ String action = intent.getAction();
+ if (ACTION_INSTALLED.equals(action)) {
+ String pkgName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
+ try {
+ PackageInfo pi = getPackageManager().getPackageInfo(pkgName, 0);
+ if (pi != null) {
+ if ((pi.themeInfos != null && pi.themeInfos.length > 0) ||
+ (pi.legacyThemeInfos != null && pi.legacyThemeInfos.length > 0)) {
+ cancelNotification(GOOGLE_PLAY_PACKAGE_NAME, sbn.getTag(), sbn.getId());
+ }
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onNotificationRemoved(StatusBarNotification sbn) {
+ }
+
+ // ensure that this notification listener is enabled.
+ // the service watches for google play notifications
+ public static void ensureEnabled(Context context) {
+ ComponentName me = new ComponentName(context, NotificationHijackingService.class);
+ String meFlattened = me.flattenToString();
+
+ String existingListeners = Settings.Secure.getString(context.getContentResolver(),
+ Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
+
+ if (!TextUtils.isEmpty(existingListeners)) {
+ if (existingListeners.contains(meFlattened)) {
+ return;
+ } else {
+ existingListeners += ":" + meFlattened;
+ }
+ } else {
+ existingListeners = meFlattened;
+ }
+
+ Settings.Secure.putString(context.getContentResolver(),
+ Settings.Secure.ENABLED_NOTIFICATION_LISTENERS,
+ existingListeners);
+ }
+} \ No newline at end of file
diff --git a/src/org/cyanogenmod/theme/chooser/WallpaperAndIconPreviewFragment.java b/src/org/cyanogenmod/theme/chooser/WallpaperAndIconPreviewFragment.java
new file mode 100644
index 0000000..da8d1a6
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/WallpaperAndIconPreviewFragment.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.chooser;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.AsyncTaskLoader;
+import android.support.v4.content.Loader;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.webkit.URLUtil;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.cyanogenmod.theme.util.IconPreviewHelper;
+import org.cyanogenmod.theme.util.Utils;
+
+public class WallpaperAndIconPreviewFragment extends Fragment
+{
+ private static final int LOADER_ID_IMAGE = 0;
+ private static final int LOADER_ID_ICONS = 1;
+
+ private static final ComponentName COMPONENT_DIALER = new ComponentName("com.android.dialer",
+ "com.android.dialer.DialtactsActivity");
+ private static final ComponentName COMPONENT_MESSAGING = new ComponentName("com.android.mms",
+ "com.android.mms.ui.ConversationList");
+
+ private static final ComponentName COMPONENT_CAMERANEXT = new ComponentName("com.cyngn.cameranext",
+ "com.android.camera.CameraLauncher");
+
+ private static final ComponentName COMPONENT_BROWSER = new ComponentName("com.android.browser",
+ "com.android.browser.BrowserActivity");
+
+ public static final ComponentName[] ICON_COMPONENTS = { COMPONENT_DIALER, COMPONENT_MESSAGING, COMPONENT_CAMERANEXT,
+ COMPONENT_BROWSER };
+
+ private static final String PKGNAME_EXTRA = "pkgname";
+ private static final String IMAGE_DATA_EXTRA = "url";
+ private static final String LEGACY_THEME_EXTRA = "isLegacyTheme";
+ private static final String HAS_ICONS_EXTRA = "hasIcons";
+
+ public static final String FRAMEWORK_RES = "/system/framework/framework-res.apk";
+
+ private String mPkgName;
+ private String mImageUrl;
+ private boolean mIsLegacyTheme;
+ private boolean mHasIcons;
+
+ private ImageView mImageView;
+ private LinearLayout mIconContainer;
+
+ static WallpaperAndIconPreviewFragment newInstance(String imageUrl, String pkgName, boolean isLegacyTheme, boolean hasIcons) {
+ final WallpaperAndIconPreviewFragment f = new WallpaperAndIconPreviewFragment();
+ final Bundle args = new Bundle();
+ args.putString(IMAGE_DATA_EXTRA, imageUrl);
+ args.putString(PKGNAME_EXTRA, pkgName);
+ args.putBoolean(LEGACY_THEME_EXTRA, isLegacyTheme);
+ args.putBoolean(HAS_ICONS_EXTRA, hasIcons);
+ f.setArguments(args);
+ return f;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mImageUrl = getArguments().getString(IMAGE_DATA_EXTRA);
+ mIsLegacyTheme = getArguments().getBoolean(LEGACY_THEME_EXTRA);
+ mHasIcons = getArguments().getBoolean(HAS_ICONS_EXTRA);
+ mPkgName = getArguments().getString(PKGNAME_EXTRA);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.image_preview_item, container, false);
+ mImageView = (ImageView) view.findViewById(R.id.image);
+ mIconContainer = (LinearLayout) view.findViewById(R.id.icon_container);
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ getLoaderManager().initLoader(LOADER_ID_IMAGE, null, mImageCallbacks);
+ if (mHasIcons) {
+ getLoaderManager().initLoader(LOADER_ID_ICONS, null, mIconCallbacks);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ }
+
+ private final LoaderCallbacks<Bitmap> mImageCallbacks = new LoaderCallbacks<Bitmap>() {
+
+ @Override
+ public Loader<Bitmap> onCreateLoader(int id, Bundle args) {
+ return new ImageLoader(getActivity(), mIsLegacyTheme, mPkgName, mImageUrl);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Bitmap> loader, Bitmap result) {
+ mImageView.setImageBitmap(result);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Bitmap> loader) {
+ }
+ };
+
+ private final LoaderCallbacks<List<IconInfo>> mIconCallbacks = new LoaderCallbacks<List<IconInfo>>() {
+ @Override
+ public Loader<List<IconInfo>> onCreateLoader(int id, Bundle args) {
+ return new IconsLoader(getActivity(), mPkgName);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<List<IconInfo>> loader, List<IconInfo> infos) {
+ final float SHADOW_LARGE_RADIUS = 4.0f;
+ final float SHADOW_Y_OFFSET = 2.0f;
+ final int SHADOW_LARGE_COLOUR = 0xDD000000;
+
+ mIconContainer.removeAllViews();
+ for (IconInfo info : infos) {
+ LinearLayout.LayoutParams lparams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT, 1f);
+ TextView tv = new TextView(loader.getContext());
+ tv.setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR);
+ tv.setTextColor(Color.WHITE);
+ tv.setGravity(Gravity.CENTER_HORIZONTAL);
+ tv.setLayoutParams(lparams);
+ tv.setCompoundDrawablesWithIntrinsicBounds(null, info.icon, null, null);
+ tv.setText(info.name);
+
+ mIconContainer.addView(tv);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<List<IconInfo>> loader) {
+ }
+
+ };
+
+ public static class ImageLoader extends AsyncTaskLoader<Bitmap> {
+ private String mPkgName;
+ private boolean mIsLegacyTheme;
+ private String mImageUrl;
+ private Point mDisplaySize = new Point();
+
+ public ImageLoader(Context context, boolean isLegacyTheme, String pkgName, String imageUrl) {
+ super(context);
+ mIsLegacyTheme = isLegacyTheme;
+ mPkgName = pkgName;
+ mImageUrl = imageUrl;
+ WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ display.getSize(mDisplaySize);
+ onContentChanged();
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (takeContentChanged()) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ public Bitmap loadInBackground() {
+ Bitmap bitmap = null;
+
+ if (mIsLegacyTheme) {
+ return loadLegacyImage();
+ }
+
+ if ("default".equals(mPkgName)) {
+ Resources res = getContext().getResources();
+ AssetManager assets = new AssetManager();
+ assets.addAssetPath(FRAMEWORK_RES);
+ Resources frameworkRes = new Resources(assets, res.getDisplayMetrics(),
+ res.getConfiguration());
+ bitmap = BitmapFactory.decodeResource(frameworkRes,
+ com.android.internal.R.drawable.default_wallpaper);
+ } else {
+ if (URLUtil.isAssetUrl(mImageUrl)) {
+ bitmap = Utils.getBitmapFromAsset(getContext(), mImageUrl, mDisplaySize.x,
+ mDisplaySize.y);
+ } else {
+ bitmap = BitmapFactory.decodeFile(mImageUrl);
+ }
+ }
+ return bitmap;
+ }
+
+ private Bitmap loadLegacyImage() {
+ Bitmap bitmap;
+ try {
+ PackageManager pm = getContext().getPackageManager();
+ PackageInfo pi = pm.getPackageInfo(mPkgName, 0);
+ final Context themeContext = getContext().createPackageContext(mPkgName,
+ Context.CONTEXT_IGNORE_SECURITY);
+ final Resources res = themeContext.getResources();
+ bitmap = BitmapFactory.decodeResource(res, pi.legacyThemeInfos[0].previewResourceId);
+ } catch (PackageManager.NameNotFoundException e) {
+ bitmap = null;
+ }
+ return bitmap;
+ }
+ }
+
+ public static class IconsLoader extends AsyncTaskLoader<List<IconInfo>> {
+ private String mPkgName;
+
+ public IconsLoader(Context context, String pkgName) {
+ super(context);
+ mPkgName = pkgName;
+ onContentChanged();
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (takeContentChanged()) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ public List<IconInfo> loadInBackground() {
+ List<IconInfo> icons = new ArrayList<IconInfo>();
+ IconPreviewHelper helper = new IconPreviewHelper(getContext(), mPkgName);
+
+ for (ComponentName component : ICON_COMPONENTS) {
+ Drawable icon = helper.getIcon(component);
+ String label = helper.getLabel(component);
+ IconInfo info = new IconInfo(label, icon);
+ icons.add(info);
+ }
+ return icons;
+ }
+ }
+
+ public static class IconInfo {
+ public String name;
+ public Drawable icon;
+ public IconInfo(String name, Drawable drawable) {
+ this.name = name;
+ this.icon = drawable;
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/BootAnimationHelper.java b/src/org/cyanogenmod/theme/util/BootAnimationHelper.java
new file mode 100644
index 0000000..7322d20
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/BootAnimationHelper.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.util;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.view.View;
+import android.widget.ImageView;
+import org.cyanogenmod.theme.widget.PartAnimationDrawable;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class BootAnimationHelper {
+ public static final String THEME_INTERNAL_BOOT_ANI_PATH =
+ "assets/bootanimation/bootanimation.zip";
+ public static final String SYSTEM_BOOT_ANI_PATH = "/system/media/bootanimation.zip";
+
+ /**
+ * Takes an InputStream to a bootanimation.zip and turns it into a set of
+ * PartAnimationDrawables which can be played inside an ImageView
+ * @param is InputStream to the bootanimation.zip to process
+ * @return The list of ParteAnimationDrawables loaded
+ * @throws IOException
+ */
+ public static List<PartAnimationDrawable> loadAnimation(Context context, InputStream is) throws IOException {
+ ZipInputStream zis = (is instanceof ZipInputStream) ? (ZipInputStream) is
+ : new ZipInputStream(new BufferedInputStream(is));
+ ZipEntry ze;
+ Map<String, TreeMap<String, Drawable>> framesMap =
+ new HashMap<String, TreeMap<String, Drawable>>();
+ List<AnimationPart> animationParts = null;
+ ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inSampleSize = am.isLowRamDevice() ? 4 : 2;
+ opts.inPreferredConfig = Bitmap.Config.RGB_565;
+ // First thing to do is iterate over all the entries and the zip and store them
+ // for building the animations afterwards
+ while ((ze = zis.getNextEntry()) != null) {
+ final String entryName = ze.getName();
+ if ("desc.txt".equals(entryName)) {
+ animationParts = parseDescription(zis);
+ } else if (entryName.contains("/") && !entryName.endsWith("/")) {
+ int splitAt = entryName.lastIndexOf('/');
+ final String part = entryName.substring(0, splitAt);
+ final String name = entryName.substring(splitAt + 1);
+ final Drawable d;
+ try {
+ d = loadFrame(zis, opts);
+ } catch (OutOfMemoryError oome) {
+ // better to have something rather than nothing?
+ break;
+ }
+ TreeMap<String, Drawable> parts = framesMap.get(part);
+ if (parts == null) {
+ parts = new TreeMap<String, Drawable>();
+ framesMap.put(part, parts);
+ }
+ parts.put(name, d);
+ }
+ }
+ zis.close();
+ if (animationParts == null) return null;
+
+ // Now that the desc.txt and images are loaded we can assemble the variouse
+ // parts into one PartAnimationDrawable per part
+ List<PartAnimationDrawable> animations = new ArrayList<PartAnimationDrawable>(animationParts.size());
+ for (AnimationPart a : animationParts) {
+ PartAnimationDrawable anim = new PartAnimationDrawable();
+ anim.setPlayCount(a.playCount);
+ final TreeMap<String, Drawable> parts = framesMap.get(a.partName);
+ for (Drawable d : parts.values()) {
+ anim.addFrame(d, a.frameRateMillis);
+ }
+ if (a.playCount <= 0) {
+ anim.setOneShot(false);
+ } else {
+ anim.setOneShot(true);
+ }
+ animations.add(anim);
+ }
+
+ return animations;
+ }
+
+ /**
+ * Parses the desc.txt of the boot animation
+ * @param in InputStream to the desc.txt
+ * @return A list of the parts as given in desc.txt
+ * @throws IOException
+ */
+ private static List<AnimationPart> parseDescription(InputStream in) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ // first line, 3rd column has # of frames per second
+ final int frameRateMillis = 1000 / Integer.parseInt(reader.readLine().split(" ")[2]);
+ String line;
+ List<AnimationPart> animationParts = new ArrayList<AnimationPart>();
+ while ((line = reader.readLine()) != null) {
+ String[] info = line.split(" ");
+ if (info.length == 4 && info[0].equals("p")) {
+ int playCount = Integer.parseInt(info[1]);
+ int pause = Integer.parseInt(info[2]);
+ String name = info[3];
+ AnimationPart ap = new AnimationPart(playCount, pause, name, frameRateMillis);
+ animationParts.add(ap);
+ }
+ }
+
+ return animationParts;
+ }
+
+ /**
+ * Load a frame of the boot animation into a BitmapDrawable
+ * @param is The frame to load
+ * @param opts Options to use when decoding the bitmap
+ * @return The loaded BitmapDrawable
+ * @throws FileNotFoundException
+ */
+ private static BitmapDrawable loadFrame(InputStream is, BitmapFactory.Options opts)
+ throws FileNotFoundException {
+ BitmapDrawable drawable = new BitmapDrawable(BitmapFactory.decodeStream(is, null, opts));
+ drawable.setAntiAlias(true);
+ drawable.setFilterBitmap(true);
+ return drawable;
+ }
+
+ private static class AnimationPart {
+ public int playCount;
+ public int pause;
+ String partName;
+ int frameRateMillis;
+
+ public AnimationPart(int playCount, int pause, String partName, int frameRateMillis) {
+ this.playCount = playCount;
+ this.pause = pause;
+ this.partName = partName;
+ this.frameRateMillis = frameRateMillis;
+ }
+ }
+
+ public static String getPreviewFrameEntryName(InputStream is) throws IOException {
+ ZipInputStream zis = (is instanceof ZipInputStream) ? (ZipInputStream) is
+ : new ZipInputStream(new BufferedInputStream(is));
+ ZipEntry ze;
+ // First thing to do is iterate over all the entries and the zip and store them
+ // for building the animations afterwards
+ String previewName = null;
+ while ((ze = zis.getNextEntry()) != null) {
+ final String entryName = ze.getName();
+ if (entryName.contains("/")
+ && (entryName.endsWith(".png") || entryName.endsWith(".jpg"))) {
+ previewName = entryName;
+ }
+ }
+
+ return previewName;
+ }
+
+ public static Bitmap loadPreviewFrame(Context context, InputStream is, String previewName)
+ throws IOException {
+ ZipInputStream zis = (is instanceof ZipInputStream) ? (ZipInputStream) is
+ : new ZipInputStream(new BufferedInputStream(is));
+ ZipEntry ze;
+ ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inSampleSize = am.isLowRamDevice() ? 4 : 2;
+ opts.inPreferredConfig = Bitmap.Config.RGB_565;
+ // First thing to do is iterate over all the entries and the zip and store them
+ // for building the animations afterwards
+ Bitmap preview = null;
+ while ((ze = zis.getNextEntry()) != null && preview == null) {
+ final String entryName = ze.getName();
+ if (entryName.equals(previewName)) {
+ preview = BitmapFactory.decodeStream(zis, null, opts);
+ }
+ }
+ zis.close();
+
+ return preview;
+ }
+
+ public static class LoadBootAnimationImage extends AsyncTask<Object, Void, Bitmap> {
+ private ImageView imv;
+ private String path;
+ private Context context;
+
+ public LoadBootAnimationImage(ImageView imv, Context context, String path) {
+ this.imv = imv;
+ this.context = context;
+ this.path = path;
+ }
+
+ @Override
+ protected Bitmap doInBackground(Object... params) {
+ Bitmap bitmap = null;
+ String previewName = null;
+ // this is ugly, ugly, ugly. Did I mention this is ugly?
+ try {
+ if ("default".equals(path)) {
+ previewName = getPreviewFrameEntryName(
+ new FileInputStream(SYSTEM_BOOT_ANI_PATH));
+ bitmap = loadPreviewFrame(
+ context, new FileInputStream(SYSTEM_BOOT_ANI_PATH), previewName);
+ } else {
+ final Context themeCtx = context.createPackageContext(path, 0);
+ previewName = getPreviewFrameEntryName(
+ themeCtx.getAssets().open("bootanimation/bootanimation.zip"));
+ bitmap = loadPreviewFrame(context,
+ themeCtx.getAssets().open("bootanimation/bootanimation.zip"),
+ previewName);
+ }
+ } catch (Exception e) {
+ // don't care since a null bitmap will be returned
+ e.printStackTrace();
+ }
+ return bitmap;
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap result) {
+ if (result != null && imv != null) {
+ imv.setVisibility(View.VISIBLE);
+ imv.setImageBitmap(result);
+ }
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/CustomTypeFaceSpan.java b/src/org/cyanogenmod/theme/util/CustomTypeFaceSpan.java
new file mode 100644
index 0000000..15650cd
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/CustomTypeFaceSpan.java
@@ -0,0 +1,55 @@
+/*
+ * Author: Laurence Dawson
+ * Source: http://stackoverflow.com/questions/9618835/apply-two-different-font-styles-to-a-textview
+ */
+package org.cyanogenmod.theme.util;
+
+
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+import android.text.style.TypefaceSpan;
+
+public class CustomTypeFaceSpan extends TypefaceSpan {
+
+ public Typeface mTf;
+
+ public CustomTypeFaceSpan(Typeface tf) {
+ super("");
+ mTf = tf;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ apply(ds);
+
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint paint) {
+ apply(paint);
+ }
+
+ private void apply(Paint paint) {
+ int oldStyle;
+
+ Typeface old = paint.getTypeface();
+ if (old == null) {
+ oldStyle = 0;
+ } else {
+ oldStyle = old.getStyle();
+ }
+
+ int fake = oldStyle & ~mTf.getStyle();
+
+ if ((fake & Typeface.BOLD) != 0) {
+ paint.setFakeBoldText(true);
+ }
+
+ if ((fake & Typeface.ITALIC) != 0) {
+ paint.setTextSkewX(-0.25f);
+ }
+
+ paint.setTypeface(mTf);
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/FittedTextView.java b/src/org/cyanogenmod/theme/util/FittedTextView.java
new file mode 100644
index 0000000..cbfe5b9
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/FittedTextView.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.util;
+
+import android.content.Context;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.TextView;
+
+/**
+ * Change the font size to match the measured
+ * textview size by width
+ *
+ */
+public class FittedTextView extends TextView {
+ private Paint mPaint;
+
+ public FittedTextView(Context context) {
+ super(context);
+ mPaint = new Paint();
+ }
+
+ public FittedTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mPaint = new Paint();
+ }
+
+ public FittedTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mPaint = new Paint();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ final float THRESHOLD = 0.5f;
+ final float TARGET_WIDTH = getMeasuredWidth();
+ final String text = getText().toString();
+ mPaint.set(getPaint());
+
+ float max = 200;
+ float min = 2;
+ while(max > min) {
+ float size = (max+min) / 2;
+ mPaint.setTextSize(size);
+ float measuredWidth = mPaint.measureText(text);
+ if (Math.abs(TARGET_WIDTH - measuredWidth) <= THRESHOLD) {
+ break;
+ } else if (measuredWidth > TARGET_WIDTH) {
+ max = size-1;
+ } else {
+ min = size+1;
+ }
+ }
+ this.setTextSize(TypedValue.COMPLEX_UNIT_PX, min-1);
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/FontConfigParser.java b/src/org/cyanogenmod/theme/util/FontConfigParser.java
new file mode 100644
index 0000000..1f0b1b1
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/FontConfigParser.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.util;
+
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Parses an XML font config. Example:
+ *
+ *<familyset>
+ *
+ * <family>
+ * <nameset>
+ * <name>sans-serif</name>
+ * <name>arial</name>
+ * </nameset>
+ * <fileset>
+ * <file>Roboto-Regular.ttf</file>
+ * <file>Roboto-Bold.ttf</file>
+ * <file>Roboto-Italic.ttf</file>
+ * <file>Roboto-BoldItalic.ttf</file>
+ * </fileset>
+ * </family>
+ * <family>
+ * ...
+ * </family>
+ *</familyset>
+ */
+public class FontConfigParser {
+
+ public static class Family {
+ public List<String> nameset = new ArrayList<String>();
+ public List<String> fileset = new ArrayList<String>();
+ }
+
+ public static List<Family> parse(InputStream in) throws XmlPullParserException, IOException {
+ try {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(in, null);
+ parser.nextTag();
+ return readFamilySet(parser);
+ } finally {
+ in.close();
+ }
+ }
+
+ private static List<Family> readFamilySet(XmlPullParser parser) throws XmlPullParserException, IOException {
+ List<Family> families = new ArrayList<Family>();
+ parser.require(XmlPullParser.START_TAG, null, "familyset");
+
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ continue;
+ }
+ String name = parser.getName();
+
+ // Starts by looking for the entry tag
+ if (name.equals("family")) {
+ Family family = readFamily(parser);
+ families.add(family);
+ }
+ }
+ return families;
+ }
+
+ private static Family readFamily(XmlPullParser parser) throws XmlPullParserException, IOException {
+ Family family = new Family();
+ parser.require(XmlPullParser.START_TAG, null, "family");
+
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ continue;
+ }
+ String name = parser.getName();
+ if (name.equals("nameset")) {
+ List<String> nameset = readNameset(parser);
+ family.nameset = nameset;
+ } else if (name.equals("fileset")) {
+ List<String> fileset = readFileset(parser);
+ family.fileset = fileset;
+ } else {
+ skip(parser);
+ }
+ }
+ return family;
+ }
+
+ private static List<String> readNameset(XmlPullParser parser) throws XmlPullParserException, IOException {
+ List<String> names = new ArrayList<String>();
+ parser.require(XmlPullParser.START_TAG, null, "nameset");
+
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ continue;
+ }
+ String tagname = parser.getName();
+ if (tagname.equals("name")) {
+ String name = readText(parser);
+ names.add(name);
+ } else {
+ skip(parser);
+ }
+ }
+ return names;
+ }
+
+ private static List<String> readFileset(XmlPullParser parser) throws XmlPullParserException, IOException {
+ List<String> files = new ArrayList<String>();
+ parser.require(XmlPullParser.START_TAG, null, "fileset");
+
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ continue;
+ }
+ String name = parser.getName();
+ if (name.equals("file")) {
+ String file = readText(parser);
+ files.add(file);
+ } else {
+ skip(parser);
+ }
+ }
+ return files;
+ }
+
+ // For the tags title and summary, extracts their text values.
+ private static String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String result = "";
+ if (parser.next() == XmlPullParser.TEXT) {
+ result = parser.getText();
+ parser.nextTag();
+ }
+ return result;
+ }
+
+ private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ throw new IllegalStateException();
+ }
+ int depth = 1;
+ while (depth != 0) {
+ switch (parser.next()) {
+ case XmlPullParser.END_TAG:
+ depth--;
+ break;
+ case XmlPullParser.START_TAG:
+ depth++;
+ break;
+ }
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/IconPreviewHelper.java b/src/org/cyanogenmod/theme/util/IconPreviewHelper.java
new file mode 100644
index 0000000..3edadf1
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/IconPreviewHelper.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.util;
+
+import android.app.ActivityManager;
+import android.app.IconPackHelper;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.DisplayMetrics;
+import android.util.Log;
+
+/**
+ * This class handles all the logic to build a preview icon
+ * If the system currently has a theme applied we do NOT
+ * want this code to be impacted by it. So code in this
+ * class creates special "no theme attached" resource objects
+ * to retrieve objects from.
+ */
+public class IconPreviewHelper {
+ private static final String TAG = IconPreviewHelper.class.getSimpleName();
+ private final static float ICON_SCALE_FACTOR = 1.3f; //Arbitrary. Looks good
+
+ private Context mContext;
+ private DisplayMetrics mDisplayMetrics;
+ private Configuration mConfiguration;
+ private int mIconDpi = 0;
+ private String mThemePkgName;
+
+ /**
+ * @param themePkgName - The package name of the theme we wish to preview
+ */
+ public IconPreviewHelper(Context context, String themePkgName) {
+ mContext = context;
+ mDisplayMetrics = context.getResources().getDisplayMetrics();
+ mConfiguration = context.getResources().getConfiguration();
+ ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ mIconDpi = (int) (am.getLauncherLargeIconDensity() * ICON_SCALE_FACTOR);
+ mThemePkgName = themePkgName;
+ }
+
+ /**
+ * Returns the actual label name for a given component
+ * If the activity does not have a label it will return app's label
+ * If neither has a label returns empty string
+ */
+ public String getLabel(ComponentName component) {
+ String label = "";
+ try {
+ PackageManager pm = mContext.getPackageManager();
+ ApplicationInfo appInfo = pm.getApplicationInfo(component.getPackageName(), 0);
+ ActivityInfo activityInfo = pm.getActivityInfo(component, 0);
+
+ AssetManager assets = new AssetManager();
+ assets.addAssetPath(appInfo.publicSourceDir);
+ Resources res = new Resources(assets, mDisplayMetrics, mConfiguration);
+
+ if (activityInfo.labelRes != 0) {
+ label = res.getString(activityInfo.labelRes);
+ } else if (appInfo.labelRes != 0) {
+ label = res.getString(appInfo.labelRes);
+ }
+ } catch(NameNotFoundException exception) {
+ Log.e(TAG, "unable to find pkg for " + component.toString());
+ }
+ return label;
+ }
+
+ /**
+ * Returns the icon for the given component regardless of the system's
+ * currently applied theme. If the preview theme does not support the icon, then
+ * return the system default icon.
+ */
+ public Drawable getIcon(ComponentName component) {
+ String packageName = component.getPackageName();
+ String activityName = component.getClassName();
+ Drawable icon = getThemedIcon(packageName, activityName);
+ if (icon == null) {
+ icon = getIconNoTheme(packageName, activityName);
+ }
+ return icon;
+ }
+
+ private Drawable getThemedIcon(String pkgName, String activityName) {
+ Drawable drawable = null;
+ IconPackHelper iconHelper = new IconPackHelper(mContext);
+ try {
+ iconHelper.loadIconPack(mThemePkgName);
+ ActivityInfo info = new ActivityInfo();
+ info.packageName = pkgName;
+ info.name = activityName;
+ drawable = iconHelper.getDrawableForActivityWithDensity(info, mIconDpi);
+ } catch (NameNotFoundException e) {
+ Log.v(TAG, "Unable to load icon for " + pkgName + "/" + activityName);
+ }
+ return drawable;
+ }
+
+ private Drawable getIconNoTheme(String pkgName, String activityName) {
+ Drawable drawable = null;
+ ComponentName component = new ComponentName(pkgName, activityName);
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ ActivityInfo info = pm.getActivityInfo(component, 0);
+ ApplicationInfo appInfo = pm.getApplicationInfo(pkgName, 0);
+
+ AssetManager assets = new AssetManager();
+ assets.addAssetPath(appInfo.publicSourceDir);
+ Resources res = new Resources(assets, mDisplayMetrics, mConfiguration);
+
+ final int iconId = info.icon != 0 ? info.icon : appInfo.icon;
+ drawable = getFullResIcon(res, iconId);
+ } catch (NameNotFoundException e2) {
+ Log.w(TAG, "Unable to get the icon for " + pkgName + " using default");
+ }
+ drawable = (drawable != null) ? drawable : getFullResDefaultActivityIcon();
+ return drawable;
+ }
+
+ private Drawable getFullResIcon(Resources resources, int iconId) {
+ Drawable d;
+ try {
+ d = resources.getDrawableForDensity(iconId, mIconDpi);
+ } catch (Resources.NotFoundException e) {
+ d = null;
+ }
+ return (d != null) ? d : getFullResDefaultActivityIcon();
+ }
+
+ private Drawable getFullResDefaultActivityIcon() {
+ return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon);
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/NotificationHelper.java b/src/org/cyanogenmod/theme/util/NotificationHelper.java
new file mode 100644
index 0000000..a4e8b0f
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/NotificationHelper.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.util;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.graphics.BitmapFactory;
+import android.text.TextUtils;
+import org.cyanogenmod.theme.chooser.ChooserActivity;
+import org.cyanogenmod.theme.chooser.R;
+
+public class NotificationHelper {
+ public static void postThemeInstalledNotification(Context context, String pkgName) {
+ String themeName = null;
+ try {
+ PackageInfo pi = context.getPackageManager().getPackageInfo(pkgName, 0);
+ if (pi.themeInfos != null && pi.themeInfos.length > 0) {
+ themeName = pi.themeInfos[0].name;
+ } else if (pi.legacyThemeInfos != null && pi.legacyThemeInfos[0] != null) {
+ themeName = pi.legacyThemeInfos[0].name;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ return;
+ }
+ if (TextUtils.isEmpty(themeName)) {
+ return;
+ }
+
+ Intent intent = new Intent(context, ChooserActivity.class);
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.putExtra("pkgName", pkgName);
+ PendingIntent pi = PendingIntent.getActivity(context, 0, intent, 0);
+
+ NotificationManager nm =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ Notification notice = new Notification.Builder(context)
+ .setAutoCancel(true)
+ .setOngoing(false)
+ .setContentTitle(String.format(
+ context.getString(R.string.theme_installed_notification_title), themeName))
+ .setContentText(context.getString(R.string.theme_installed_notification_text))
+ .setContentIntent(pi)
+ .setSmallIcon(R.drawable.ic_notifiy)
+ .setLargeIcon(BitmapFactory.decodeResource(context.getResources(),
+ R.drawable.ic_app_themes))
+ .setWhen(System.currentTimeMillis())
+ .build();
+ nm.notify(pkgName.hashCode(), notice);
+ }
+
+ public static void cancelNotificationForPackage(Context context, String pkgName) {
+ NotificationManager nm = (NotificationManager)
+ context.getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancel(pkgName.hashCode());
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/ThemedTypefaceHelper.java b/src/org/cyanogenmod/theme/util/ThemedTypefaceHelper.java
new file mode 100644
index 0000000..29e21dc
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/ThemedTypefaceHelper.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.util;
+
+import android.content.Context;
+import android.content.pm.ThemeUtils;
+import android.content.res.AssetManager;
+import android.graphics.Typeface;
+import android.util.Log;
+
+import org.cyanogenmod.theme.util.FontConfigParser.Family;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * Assists in loading a themes font typefaces.
+ * Will load system default if there is a load issue
+ */
+public class ThemedTypefaceHelper {
+ private static final String TAG = ThemedTypefaceHelper.class.getName();
+ private static final String FAMILY_SANS_SERIF = "sans-serif";
+ private static final String FONTS_DIR = "fonts/";
+ private static final String SYSTEM_FONTS_XML = "/system/etc/system_fonts.xml";
+ private static final String SYSTEM_FONTS_DIR = "/system/fonts/";
+
+ private boolean mIsLoaded;
+ private Context mThemeContext;
+ private List<Family> mFamilies;
+ private Typeface[] mTypefaces = new Typeface[4];
+
+ public void load(Context context, String pkgName) {
+ try {
+ loadThemedFonts(context, pkgName);
+ return;
+ } catch(Exception e) {
+ Log.e(TAG, "Unable to parse and load themed fonts. Falling back to system fonts", e);
+ }
+
+ try {
+ loadSystemFonts();
+ return;
+ } catch(Exception e) {
+ Log.e(TAG, "Parsing system fonts failed. Falling back to Typeface loaded fonts");
+
+ }
+
+ // There is no reason for this to happen unless someone
+ // messed up the system_fonts.xml
+ loadDefaultFonts();
+ }
+
+ private void loadThemedFonts(Context context, String pkgName) throws Exception {
+ //Parse the font XML
+ mThemeContext = context.createPackageContext(pkgName, Context.CONTEXT_IGNORE_SECURITY);
+ AssetManager assetManager = mThemeContext.getAssets();
+ InputStream is = assetManager.open(FONTS_DIR + ThemeUtils.FONT_XML);
+ mFamilies = FontConfigParser.parse(is);
+
+ //Load the typefaces for sans-serif
+ Family sanSerif = getFamily(FAMILY_SANS_SERIF);
+ mTypefaces[Typeface.NORMAL] = loadTypeface(sanSerif, Typeface.NORMAL);
+ mTypefaces[Typeface.BOLD] = loadTypeface(sanSerif, Typeface.BOLD);
+ mTypefaces[Typeface.ITALIC] = loadTypeface(sanSerif, Typeface.ITALIC);
+ mTypefaces[Typeface.BOLD_ITALIC] = loadTypeface(sanSerif, Typeface.BOLD_ITALIC);
+ mIsLoaded = true;
+ }
+
+ private void loadSystemFonts() throws Exception {
+ //Parse the system font XML
+ File file = new File(SYSTEM_FONTS_XML);
+ InputStream is = new FileInputStream(file);
+ mFamilies = FontConfigParser.parse(is);
+
+ //Load the typefaces for sans-serif
+ Family sanSerif = getFamily(FAMILY_SANS_SERIF);
+ mTypefaces[Typeface.NORMAL] = loadSystemTypeface(sanSerif, Typeface.NORMAL);
+ mTypefaces[Typeface.BOLD] = loadSystemTypeface(sanSerif, Typeface.BOLD);
+ mIsLoaded = true;
+ }
+
+ private void loadDefaultFonts() {
+ mTypefaces[Typeface.NORMAL] = Typeface.DEFAULT;
+ mTypefaces[Typeface.BOLD] = Typeface.DEFAULT_BOLD;
+ mIsLoaded = true;
+ }
+
+ private Family getFamily(String familyName) throws Exception {
+ for(Family family : mFamilies) {
+ if (family.nameset.contains(familyName)) {
+ return family;
+ }
+ }
+ throw new Exception("Unable to find " + familyName);
+ }
+
+ private Typeface loadTypeface(Family family, int style) {
+ AssetManager assets = mThemeContext.getAssets();
+ String path = FONTS_DIR + family.fileset.get(style);
+ return Typeface.createFromAsset(assets, path);
+ }
+
+ private Typeface loadSystemTypeface(Family family, int style) {
+ return Typeface.createFromFile(SYSTEM_FONTS_DIR + family.fileset.get(style));
+ }
+
+ public Typeface getTypeface(int style) {
+ if (!mIsLoaded) throw new IllegalStateException("Helper was not loaded");
+ return mTypefaces[style];
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/Utils.java b/src/org/cyanogenmod/theme/util/Utils.java
new file mode 100644
index 0000000..27e854d
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/Utils.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.util;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Rect;
+import android.util.Log;
+import android.util.TypedValue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class Utils {
+ private static final String TAG = Utils.class.getSimpleName();
+
+ public static Bitmap decodeFile(String path, int reqWidth, int reqHeight) {
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+
+ // Determine insample size
+ opts.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, opts);
+ opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight);
+
+ // Decode the bitmap, regionally if necessary
+ Bitmap bitmap = null;
+ opts.inJustDecodeBounds = false;
+ Rect rect = getCropRectIfNecessary(opts, reqWidth, reqHeight);
+ try {
+ if (rect != null) {
+ BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(path, false);
+ // Check if we can downsample more now that we cropped
+ opts.inSampleSize = calculateInSampleSize(rect.width(), rect.height(),
+ reqWidth, reqHeight);
+ bitmap = decoder.decodeRegion(rect, opts);
+ } else {
+ bitmap = BitmapFactory.decodeFile(path, opts);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to open resource in path" + path, e);
+ }
+ return bitmap;
+ }
+
+ public static Bitmap decodeResource(Resources res, int resId, int reqWidth, int reqHeight) {
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+
+ // Determine insample size
+ opts.inJustDecodeBounds = true;
+ BitmapFactory.decodeResource(res, resId, opts);
+ opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight);
+
+ // Decode the bitmap, regionally if necessary
+ Bitmap bitmap = null;
+ opts.inJustDecodeBounds = false;
+ Rect rect = getCropRectIfNecessary(opts, reqWidth, reqHeight);
+
+ InputStream stream = null;
+ try {
+ if (rect != null) {
+ stream = res.openRawResource(resId, new TypedValue());
+ if (stream == null) return null;
+ BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(stream, false);
+ // Check if we can downsample a little more now that we cropped
+ opts.inSampleSize = calculateInSampleSize(rect.width(), rect.height(),
+ reqWidth, reqHeight);
+ bitmap = decoder.decodeRegion(rect, opts);
+ } else {
+ bitmap = BitmapFactory.decodeResource(res, resId, opts);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to open resource " + resId, e);
+ } finally {
+ closeQuiet(stream);
+ }
+ return bitmap;
+ }
+
+
+ public static Bitmap getBitmapFromAsset(Context ctx, String path,int reqWidth, int reqHeight) {
+ if (ctx == null || path == null)
+ return null;
+
+ String ASSET_BASE = "file:///android_asset/";
+ path = path.substring(ASSET_BASE.length());
+
+
+ Bitmap bitmap = null;
+ try {
+ AssetManager assets = ctx.getAssets();
+ InputStream is = assets.open(path);
+
+ // Determine insample size
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(is, null, opts);
+ opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight);
+ is.close();
+
+ // Decode the bitmap, regionally if neccessary
+ is = assets.open(path);
+ opts.inJustDecodeBounds = false;
+ Rect rect = getCropRectIfNecessary(opts, reqWidth, reqHeight);
+ if (rect != null) {
+ BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
+ // Check if we can downsample a little more now that we cropped
+ opts.inSampleSize = calculateInSampleSize(rect.width(), rect.height(),
+ reqWidth, reqHeight);
+ bitmap = decoder.decodeRegion(rect, opts);
+ } else {
+ bitmap = BitmapFactory.decodeStream(is);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return bitmap;
+ }
+
+
+ /**
+ * For excessively large images with an awkward ratio we
+ * will want to crop them
+ * @return
+ */
+ public static Rect getCropRectIfNecessary(
+ BitmapFactory.Options options,int reqWidth, int reqHeight) {
+ Rect rect = null;
+ // Determine downsampled size
+ int width = options.outWidth / options.inSampleSize;
+ int height = options.outHeight / options.inSampleSize;
+
+ if ((reqHeight * 1.5 < height)) {
+ int bottom = height/ 4;
+ int top = bottom + height/2;
+ rect = new Rect(0, bottom, width, top);
+ } else if ((reqWidth * 1.5 < width)) {
+ int left = width / 4;
+ int right = left + height/2;
+ rect = new Rect(left, 0, right, height);
+ }
+ return rect;
+ }
+
+ public static int calculateInSampleSize(
+ BitmapFactory.Options options, int reqWidth, int reqHeight) {
+ return calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight);
+ }
+
+ // Modified from original source:
+ // http://developer.android.com/training/displaying-bitmaps/load-bitmap.html
+ public static int calculateInSampleSize(
+ int decodeWidth, int decodeHeight, int reqWidth, int reqHeight) {
+ // Raw height and width of image
+ int inSampleSize = 1;
+
+ if (decodeHeight > reqHeight || decodeWidth > reqWidth) {
+ final int halfHeight = decodeHeight / 2;
+ final int halfWidth = decodeWidth / 2;
+
+ // Calculate the largest inSampleSize value that is a power of 2 and keeps both
+ // height and width larger than the requested height and width.
+ while ((halfHeight / inSampleSize) > reqHeight &&
+ (halfWidth / inSampleSize) > reqWidth) {
+ inSampleSize *= 2;
+ }
+ }
+
+ return inSampleSize;
+ }
+
+ public static InputStream getInputStreamFromAsset(
+ Context ctx, String path) throws IOException {
+ if (ctx == null || path == null)
+ return null;
+ InputStream is = null;
+ String ASSET_BASE = "file:///android_asset/";
+ path = path.substring(ASSET_BASE.length());
+ AssetManager assets = ctx.getAssets();
+ is = assets.open(path);
+ return is;
+ }
+
+ public static void copy(InputStream is, OutputStream os) throws IOException {
+ final byte[] bytes = new byte[4096];
+ int len;
+ while ((len = is.read(bytes)) > 0) {
+ os.write(bytes, 0, len);
+ }
+ }
+
+ public static void closeQuiet(InputStream stream) {
+ if (stream == null)
+ return;
+ try {
+ stream.close();
+ } catch (IOException e) {
+ }
+ }
+
+ public static void closeQuiet(OutputStream stream) {
+ if (stream == null)
+ return;
+ try {
+ stream.close();
+ } catch (IOException e) {
+ }
+ }
+
+ //Note: will not delete populated subdirs
+ public static void deleteFilesInDir(String dirPath) {
+ File fontDir = new File(dirPath);
+ File[] files = fontDir.listFiles();
+ if (files != null) {
+ for(File file : fontDir.listFiles()) {
+ file.delete();
+ }
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/widget/PartAnimationDrawable.java b/src/org/cyanogenmod/theme/widget/PartAnimationDrawable.java
new file mode 100644
index 0000000..c5b0bc1
--- /dev/null
+++ b/src/org/cyanogenmod/theme/widget/PartAnimationDrawable.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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 org.cyanogenmod.theme.widget;
+
+import android.graphics.drawable.AnimationDrawable;
+
+public class PartAnimationDrawable extends AnimationDrawable {
+ private int mPlayCount;
+
+ public int getPlayCount() {
+ return mPlayCount;
+ }
+
+ public void setPlayCount(int mPlayCount) {
+ this.mPlayCount = mPlayCount;
+ }
+
+ public int getAnimationDuration() {
+ return (getNumberOfFrames() - 1) * getDuration(0);
+ }
+
+ @Override
+ public void start() {
+ super.start();
+ if (isOneShot()) mPlayCount--;
+ }
+}