diff options
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 Binary files differnew file mode 100644 index 0000000..b8ebdfe --- /dev/null +++ b/assets/default_holo_theme/holo_boot_anim.jpg diff --git a/assets/default_holo_theme/holo_homescreen.png b/assets/default_holo_theme/holo_homescreen.png Binary files differnew file mode 100644 index 0000000..41624d6 --- /dev/null +++ b/assets/default_holo_theme/holo_homescreen.png diff --git a/assets/default_holo_theme/holo_lockscreen.png b/assets/default_holo_theme/holo_lockscreen.png Binary files differnew file mode 100644 index 0000000..d48e9c5 --- /dev/null +++ b/assets/default_holo_theme/holo_lockscreen.png diff --git a/assets/default_holo_theme/style.jpg b/assets/default_holo_theme/style.jpg Binary files differnew file mode 100644 index 0000000..abd397d --- /dev/null +++ b/assets/default_holo_theme/style.jpg diff --git a/res/drawable-hdpi/ic_app_themes.png b/res/drawable-hdpi/ic_app_themes.png Binary files differnew file mode 100644 index 0000000..b2bdd60 --- /dev/null +++ b/res/drawable-hdpi/ic_app_themes.png diff --git a/res/drawable-hdpi/ic_app_themes_bw.png b/res/drawable-hdpi/ic_app_themes_bw.png Binary files differnew file mode 100755 index 0000000..f8488d0 --- /dev/null +++ b/res/drawable-hdpi/ic_app_themes_bw.png diff --git a/res/drawable-mdpi/ic_app_themes.png b/res/drawable-mdpi/ic_app_themes.png Binary files differnew file mode 100644 index 0000000..9266504 --- /dev/null +++ b/res/drawable-mdpi/ic_app_themes.png diff --git a/res/drawable-mdpi/ic_app_themes_bw.png b/res/drawable-mdpi/ic_app_themes_bw.png Binary files differnew file mode 100755 index 0000000..bb48219 --- /dev/null +++ b/res/drawable-mdpi/ic_app_themes_bw.png diff --git a/res/drawable-xhdpi/ic_app_themes.png b/res/drawable-xhdpi/ic_app_themes.png Binary files differnew file mode 100644 index 0000000..85f85ee --- /dev/null +++ b/res/drawable-xhdpi/ic_app_themes.png diff --git a/res/drawable-xhdpi/ic_app_themes_bw.png b/res/drawable-xhdpi/ic_app_themes_bw.png Binary files differnew file mode 100755 index 0000000..3a65585 --- /dev/null +++ b/res/drawable-xhdpi/ic_app_themes_bw.png diff --git a/res/drawable-xhdpi/ic_drawer.png b/res/drawable-xhdpi/ic_drawer.png Binary files differnew file mode 100644 index 0000000..bcf49dd --- /dev/null +++ b/res/drawable-xhdpi/ic_drawer.png diff --git a/res/drawable-xxhdpi/ic_app_themes.png b/res/drawable-xxhdpi/ic_app_themes.png Binary files differnew file mode 100644 index 0000000..fbbf03b --- /dev/null +++ b/res/drawable-xxhdpi/ic_app_themes.png diff --git a/res/drawable-xxhdpi/ic_app_themes_bw.png b/res/drawable-xxhdpi/ic_app_themes_bw.png Binary files differnew file mode 100755 index 0000000..a1c5c4b --- /dev/null +++ b/res/drawable-xxhdpi/ic_app_themes_bw.png diff --git a/res/drawable-xxhdpi/ic_notifiy.png b/res/drawable-xxhdpi/ic_notifiy.png Binary files differnew file mode 100644 index 0000000..ff86c5c --- /dev/null +++ b/res/drawable-xxhdpi/ic_notifiy.png 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--; + } +} |