summaryrefslogtreecommitdiffstats
path: root/src/org/cyanogenmod/theme
diff options
context:
space:
mode:
authorClark Scheff <clark@scheffsblend.com>2016-08-28 09:21:05 -0700
committerClark Scheff <clark@scheffsblend.com>2016-08-28 09:21:33 -0700
commitbcd90a38b28f7c57bce8057d855e4c276aab2776 (patch)
tree9524b01c9e7c84ff3934ac035481d7eb9598478b /src/org/cyanogenmod/theme
parentde36713290379cc4e0de6e0bc2cfc25e3258133f (diff)
downloadpackages_apps_ThemeChooser-bcd90a38b28f7c57bce8057d855e4c276aab2776.tar.gz
packages_apps_ThemeChooser-bcd90a38b28f7c57bce8057d855e4c276aab2776.tar.bz2
packages_apps_ThemeChooser-bcd90a38b28f7c57bce8057d855e4c276aab2776.zip
Theme chooser for the people
Let freedom ring! Change-Id: I12b3e6b5d46eb2e13afd841dfd5c215af64188d8 TICKET: OSS-67
Diffstat (limited to 'src/org/cyanogenmod/theme')
-rw-r--r--src/org/cyanogenmod/theme/chooser/AppReceiver.java73
-rw-r--r--src/org/cyanogenmod/theme/chooser/BootReceiver.java56
-rw-r--r--src/org/cyanogenmod/theme/chooser/ChooserActivity.java1191
-rw-r--r--src/org/cyanogenmod/theme/chooser/ComponentCardView.java233
-rw-r--r--src/org/cyanogenmod/theme/chooser/ComponentSelector.java934
-rw-r--r--src/org/cyanogenmod/theme/chooser/IconTransitionDrawable.java171
-rw-r--r--src/org/cyanogenmod/theme/chooser/MyThemeFragment.java707
-rw-r--r--src/org/cyanogenmod/theme/chooser/NewFragmentStatePagerAdapter.java333
-rw-r--r--src/org/cyanogenmod/theme/chooser/NotificationHijackingService.java88
-rw-r--r--src/org/cyanogenmod/theme/chooser/PagerContainer.java219
-rw-r--r--src/org/cyanogenmod/theme/chooser/ThemeFragment.java3059
-rw-r--r--src/org/cyanogenmod/theme/chooser/WallpaperCardView.java160
-rw-r--r--src/org/cyanogenmod/theme/perapptheming/PerAppThemeListLayout.java174
-rw-r--r--src/org/cyanogenmod/theme/perapptheming/PerAppThemeListView.java74
-rw-r--r--src/org/cyanogenmod/theme/perapptheming/PerAppThemingWindow.java1078
-rw-r--r--src/org/cyanogenmod/theme/util/AudioUtils.java106
-rw-r--r--src/org/cyanogenmod/theme/util/BootAnimationHelper.java319
-rw-r--r--src/org/cyanogenmod/theme/util/CursorLoaderHelper.java447
-rw-r--r--src/org/cyanogenmod/theme/util/FontConfigParser.java175
-rw-r--r--src/org/cyanogenmod/theme/util/IconPreviewHelper.java197
-rw-r--r--src/org/cyanogenmod/theme/util/NotificationHelper.java96
-rw-r--r--src/org/cyanogenmod/theme/util/PreferenceUtils.java133
-rw-r--r--src/org/cyanogenmod/theme/util/ThemedTypefaceHelper.java141
-rw-r--r--src/org/cyanogenmod/theme/util/TypefaceHelperCache.java57
-rw-r--r--src/org/cyanogenmod/theme/util/Utils.java724
-rw-r--r--src/org/cyanogenmod/theme/util/WallpaperUtils.java440
-rw-r--r--src/org/cyanogenmod/theme/widget/AutoSnapHorizontalScrollView.java168
-rw-r--r--src/org/cyanogenmod/theme/widget/BootAniImageView.java226
-rw-r--r--src/org/cyanogenmod/theme/widget/ConfirmCancelOverlay.java89
-rw-r--r--src/org/cyanogenmod/theme/widget/FittedTextView.java98
-rw-r--r--src/org/cyanogenmod/theme/widget/LatoTextView.java184
-rw-r--r--src/org/cyanogenmod/theme/widget/LockableScrollView.java69
-rw-r--r--src/org/cyanogenmod/theme/widget/NavBarSpace.java53
-rw-r--r--src/org/cyanogenmod/theme/widget/ThemeTagLayout.java140
34 files changed, 12412 insertions, 0 deletions
diff --git a/src/org/cyanogenmod/theme/chooser/AppReceiver.java b/src/org/cyanogenmod/theme/chooser/AppReceiver.java
new file mode 100644
index 0000000..0c3037e
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/AppReceiver.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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 android.text.TextUtils;
+
+import org.cyanogenmod.theme.util.NotificationHelper;
+import org.cyanogenmod.theme.util.PreferenceUtils;
+import org.cyanogenmod.theme.util.Utils;
+
+import cyanogenmod.providers.ThemesContract;
+
+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();
+
+ if (cyanogenmod.content.Intent.ACTION_THEME_INSTALLED.equals(action)) {
+ if (!pkgName.equals(Utils.getDefaultThemePackageName(context))) {
+ NotificationHelper.postThemeInstalledNotification(context, pkgName);
+ }
+ } else if (cyanogenmod.content.Intent.ACTION_THEME_REMOVED.equals(action)) {
+ // remove updated status for this theme (if one exists)
+ PreferenceUtils.removeUpdatedTheme(context, pkgName);
+
+ // If the theme being removed was the currently applied theme we need
+ // to update the applied base theme in preferences to the default theme.
+ String appliedBaseTheme = PreferenceUtils.getAppliedBaseTheme(context);
+ if (!TextUtils.isEmpty(appliedBaseTheme) && appliedBaseTheme.equals(pkgName)) {
+ PreferenceUtils.setAppliedBaseTheme(context,
+ Utils.getDefaultThemePackageName(context));
+ }
+ NotificationHelper.cancelNotifications(context);
+ } else if (cyanogenmod.content.Intent.ACTION_THEME_UPDATED.equals(action)) {
+ try {
+ if (isTheme(context, pkgName)) {
+ PreferenceUtils.addUpdatedTheme(context, pkgName);
+ }
+ } catch (NameNotFoundException e) {
+ }
+ }
+ }
+
+ private boolean isTheme(Context context, String pkgName) throws NameNotFoundException {
+ PackageInfo pi = context.getPackageManager().getPackageInfo(pkgName, 0);
+
+ return pi != null && pi.themeInfo != null;
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/BootReceiver.java b/src/org/cyanogenmod/theme/chooser/BootReceiver.java
new file mode 100644
index 0000000..00a6723
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/BootReceiver.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+public class BootReceiver extends BroadcastReceiver {
+ private static final String CHOOSER_PKG_NAME = "org.cyanogenmod.theme.chooser";
+ private static final String CHOOSER_ACTIVITY = "org.cyanogenmod.theme.chooser.ChooserLauncher";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ PackageManager pm = context.getPackageManager();
+ if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+ try {
+ PackageInfo info = pm.getPackageInfo(ChooserActivity.THEME_STORE_PACKAGE, 0);
+ if (info != null) {
+ ComponentName cn = new ComponentName(CHOOSER_PKG_NAME, CHOOSER_ACTIVITY);
+ pm.setComponentEnabledSetting(cn,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // no store so nothing to do.
+ }
+
+ // now disable this receiver so we don't get called on future boots
+ ComponentName cn = new ComponentName(CHOOSER_PKG_NAME,
+ BootReceiver.class.getCanonicalName());
+ pm.setComponentEnabledSetting(cn,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/ChooserActivity.java b/src/org/cyanogenmod/theme/chooser/ChooserActivity.java
new file mode 100644
index 0000000..081325d
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/ChooserActivity.java
@@ -0,0 +1,1191 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.IPackageDeleteObserver;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.res.ThemeConfig;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.RenderScript;
+import android.renderscript.ScriptIntrinsicBlur;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.Loader;
+import android.support.v4.view.ThemeViewPager;
+import android.support.v4.view.ViewPager;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.MutableLong;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import org.cyanogenmod.theme.perapptheming.PerAppThemingWindow;
+import org.cyanogenmod.theme.util.CursorLoaderHelper;
+import org.cyanogenmod.theme.util.NotificationHelper;
+import org.cyanogenmod.theme.util.PreferenceUtils;
+import org.cyanogenmod.theme.util.TypefaceHelperCache;
+import org.cyanogenmod.theme.util.Utils;
+
+import cyanogenmod.platform.Manifest;
+import cyanogenmod.providers.ThemesContract;
+import cyanogenmod.providers.ThemesContract.ThemesColumns;
+import cyanogenmod.themes.ThemeManager;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_ALARMS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_BOOT_ANIM;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_NOTIFICATIONS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_RINGTONES;
+import static org.cyanogenmod.theme.chooser.ComponentSelector.DEFAULT_COMPONENT_ID;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_INSTALLED_THEMES;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_APPLIED;
+
+public class ChooserActivity extends FragmentActivity
+ implements LoaderManager.LoaderCallbacks<Cursor> {
+ public static final String THEME_STORE_PACKAGE = "com.cyngn.theme.tore";
+ private static final String TAG = ChooserActivity.class.getSimpleName();
+
+ public static final String DEFAULT = ThemeConfig.SYSTEM_DEFAULT;
+ public static final String EXTRA_PKGNAME = "pkgName";
+ public static final String EXTRA_COMPONENTS = "components";
+
+ private static final int OFFSCREEN_PAGE_LIMIT = 3;
+
+ private static final String THEME_STORE_ACTIVITY = THEME_STORE_PACKAGE + ".ui.StoreActivity";
+ private static final String ACTION_APPLY_THEME = "android.intent.action.APPLY_THEME";
+
+ private static final String TYPE_IMAGE = "image/*";
+
+ private static final String ACTION_PICK_LOCK_SCREEN_WALLPAPER =
+ "com.cyngn.intent.action.PICK_LOCK_SCREEN_WALLPAPER";
+
+ /**
+ * Request code for picking an external wallpaper
+ */
+ public static final int REQUEST_PICK_WALLPAPER_IMAGE = 2;
+ /**
+ * Request code for picking an external lockscreen wallpaper
+ */
+ public static final int REQUEST_PICK_LOCKSCREEN_IMAGE = 3;
+
+ /**
+ * Request code for enabling system alert window permission
+ */
+ private static final int REQUEST_SYSTEM_WINDOW_PERMISSION = 4;
+
+ private static final long ANIMATE_CONTENT_IN_SCALE_DURATION = 500;
+ private static final long ANIMATE_CONTENT_IN_ALPHA_DURATION = 750;
+ private static final long ANIMATE_CONTENT_IN_BLUR_DURATION = 250;
+ private static final long ANIMATE_CONTENT_DELAY = 250;
+ private static final long ANIMATE_SHOP_THEMES_HIDE_DURATION = 250;
+ private static final long ANIMATE_SHOP_THEMES_SHOW_DURATION = 500;
+ private static final long FINISH_ANIMATION_DELAY = ThemeFragment.ANIMATE_DURATION
+ + ThemeFragment.ANIMATE_START_DELAY + 250;
+
+ private static final long ANIMATE_CARDS_IN_DURATION = 250;
+ private static final long ANIMATE_SAVE_APPLY_LAYOUT_DURATION = 300;
+ private static final float ANIMATE_SAVE_APPLY_DECELERATE_INTERPOLATOR_FACTOR = 3;
+ private static final long ONCLICK_SAVE_APPLY_FINISH_ANIMATION_DELAY = 400;
+
+ private PagerContainer mContainer;
+ private ThemeViewPager mPager;
+
+ private ThemesAdapter mAdapter;
+ private boolean mExpanded = false;
+ private ComponentSelector mSelector;
+ private View mSaveApplyLayout;
+ private int mContainerYOffset = 0;
+ private TypefaceHelperCache mTypefaceHelperCache;
+ private boolean mIsAnimating;
+ private Handler mHandler;
+ private View mBottomActionsLayout;
+
+ private String mSelectedTheme;
+ private String mAppliedBaseTheme;
+ private boolean mThemeChanging = false;
+ private boolean mAnimateContentIn = false;
+ private long mAnimateContentInDelay;
+ private String mThemeToApply;
+ private ArrayList mComponentsToApply;
+
+ ImageView mCustomBackground;
+
+ // Current system theme configuration as component -> pkgName
+ private Map<String, String> mCurrentTheme = new HashMap<String, String>();
+ private MutableLong mCurrentWallpaperCmpntId = new MutableLong(DEFAULT_COMPONENT_ID);
+
+ private boolean mIsPickingImage = false;
+ private boolean mRestartLoaderOnCollapse = false;
+ private boolean mActivityResuming = false;
+ private boolean mShowLockScreenWallpaper = false;
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+ NotificationHijackingService.ensureEnabled(this);
+
+ if (savedInstanceState == null) {
+ handleIntent(getIntent());
+ }
+
+ mContainer = (PagerContainer) findViewById(R.id.pager_container);
+ mPager = (ThemeViewPager) findViewById(R.id.viewpager);
+
+ mPager.setOnClickListener(mPagerClickListener);
+ mAdapter = new ThemesAdapter();
+ mPager.setAdapter(mAdapter);
+
+ DisplayMetrics dm = getResources().getDisplayMetrics();
+ int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, dm);
+ mPager.setPageMargin(-margin / 2);
+ mPager.setOffscreenPageLimit(OFFSCREEN_PAGE_LIMIT);
+
+ mPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
+ public void onPageSelected(int position) {
+ }
+
+ public void onPageScrolled(int position,
+ float positionOffset,
+ int positionOffsetPixels) {
+ }
+
+ public void onPageScrollStateChanged(int state) {
+ }
+ });
+
+ mSelector = (ComponentSelector) findViewById(R.id.component_selector);
+ mSelector.setOnOpenCloseListener(mOpenCloseListener);
+
+ mBottomActionsLayout = findViewById(R.id.bottom_actions_layout);
+
+ mSaveApplyLayout = findViewById(R.id.save_apply_layout);
+ mSaveApplyLayout.findViewById(R.id.save_apply_button).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mIsAnimating) return;
+ hideSaveApplyButton();
+ mContainer.setClickable(false);
+ final ThemeFragment f = getCurrentFragment();
+ if (mSelector.isEnabled()) {
+ mSelector.hide();
+ if (mContainerYOffset != 0) {
+ slideContentBack(-mContainerYOffset);
+ mContainerYOffset = 0;
+ }
+ if (f != null) f.fadeInCards();
+ if (mShowLockScreenWallpaper) {
+ mShowLockScreenWallpaper = false;
+ mSelector.resetComponentType();
+ }
+ }
+
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ collapse(true);
+ }
+ }, ONCLICK_SAVE_APPLY_FINISH_ANIMATION_DELAY);
+ }
+ });
+
+ mBottomActionsLayout.findViewById(R.id.shop_themes)
+ .setOnClickListener(mOnShopThemesClicked);
+
+ mTypefaceHelperCache = TypefaceHelperCache.getInstance();
+ mHandler = new Handler();
+ mCustomBackground = (ImageView) findViewById(R.id.custom_bg);
+ mAnimateContentIn = true;
+ mAnimateContentInDelay = 0;
+
+ mBottomActionsLayout.findViewById(R.id.per_app_theming).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (Settings.canDrawOverlays(ChooserActivity.this)) {
+ launchAppThemer();
+ } else {
+ requestSystemWindowPermission();
+ }
+ }
+ });
+
+ if (PreferenceUtils.getShowPerAppThemeNewTag(this)) {
+ View tag = mBottomActionsLayout.findViewById(R.id.new_tag);
+ if (tag != null) {
+ tag.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ public void showSaveApplyButton() {
+ if (mSaveApplyLayout != null && mSaveApplyLayout.getVisibility() != View.VISIBLE) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ int navBarHeight = 0;
+ if (Utils.hasNavigationBar(ChooserActivity.this.getApplicationContext())) {
+ navBarHeight = ChooserActivity.this.getResources()
+ .getDimensionPixelSize(R.dimen.navigation_bar_height);
+ }
+ mSaveApplyLayout.setTranslationY(mSaveApplyLayout.getMeasuredHeight());
+ mSaveApplyLayout.setVisibility(View.VISIBLE);
+ mSaveApplyLayout.animate()
+ .setDuration(ANIMATE_SAVE_APPLY_LAYOUT_DURATION)
+ .setInterpolator(
+ new DecelerateInterpolator(
+ ANIMATE_SAVE_APPLY_DECELERATE_INTERPOLATOR_FACTOR))
+ .translationY(-mSelector.getMeasuredHeight()
+ + navBarHeight);
+ }
+ });
+ }
+ }
+
+ public void hideSaveApplyButton() {
+ if (mSaveApplyLayout.getVisibility() != View.GONE) {
+ Animation anim = AnimationUtils.loadAnimation(this,
+ R.anim.component_selection_animate_out);
+ mSaveApplyLayout.startAnimation(anim);
+ anim.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mSaveApplyLayout.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ });
+ }
+ }
+
+ private void hideBottomActionsLayout() {
+ final ViewPropertyAnimator anim = mBottomActionsLayout.animate();
+ anim.alpha(0f).setDuration(ANIMATE_SHOP_THEMES_HIDE_DURATION);
+ anim.setListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mBottomActionsLayout.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+ }
+
+ private void showBottomActionsLayout() {
+ mBottomActionsLayout.setVisibility(View.VISIBLE);
+ final ViewPropertyAnimator anim = mBottomActionsLayout.animate();
+ anim.setListener(null);
+ anim.alpha(1f).setStartDelay(ThemeFragment.ANIMATE_DURATION)
+ .setDuration(ANIMATE_SHOP_THEMES_SHOW_DURATION);
+ }
+
+ private void lauchGetThemes() {
+ String playStoreUrl = getString(R.string.play_store_url);
+ String wikiUrl = getString(R.string.wiki_url);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(Uri.parse(playStoreUrl));
+
+ // Try to launch play store
+ if (intent.resolveActivity(getPackageManager()) != null) {
+ startActivity(intent);
+ } else {
+ // If no play store, try to open wiki url
+ intent.setData(Uri.parse(wikiUrl));
+ if (intent.resolveActivity(getPackageManager()) != null) {
+ startActivity(intent);
+ } else {
+ Toast.makeText(this, R.string.get_more_app_not_available,
+ Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+ if (Utils.isRecentTaskHome(this)) {
+ mContainer.setAlpha(0f);
+ mBottomActionsLayout.setAlpha(0f);
+ mAnimateContentIn = true;
+ mAnimateContentInDelay = ANIMATE_CONTENT_DELAY;
+ }
+ handleIntent(intent);
+ }
+
+ private void handleIntent(Intent intent) {
+ String action = intent.getAction();
+ if ((Intent.ACTION_MAIN.equals(action) || ACTION_APPLY_THEME.equals(action))
+ && intent.hasExtra(EXTRA_PKGNAME)) {
+ if (intent.hasExtra(EXTRA_COMPONENTS)) {
+ mComponentsToApply = intent.getStringArrayListExtra(EXTRA_COMPONENTS);
+ } else {
+ mComponentsToApply = null;
+ }
+ mSelectedTheme = mComponentsToApply != null ?
+ PreferenceUtils.getAppliedBaseTheme(this) :
+ getSelectedTheme(intent.getStringExtra(EXTRA_PKGNAME));
+ if (mPager != null) {
+ startLoader(LOADER_ID_INSTALLED_THEMES);
+ if (mExpanded) {
+ int collapseDelay = ThemeFragment.ANIMATE_START_DELAY;
+ if (mSelector.isEnabled()) {
+ // onBackPressed() has all the necessary logic for collapsing the
+ // component selector, so we call it here.
+ onBackPressed();
+ collapseDelay += ThemeFragment.ANIMATE_DURATION;
+ }
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ collapse(false);
+ }
+ }, collapseDelay);
+ }
+ }
+
+ if (ACTION_APPLY_THEME.equals(action) &&
+ getCallingPackage() != null &&
+ PackageManager.PERMISSION_GRANTED ==
+ getPackageManager()
+ .checkPermission(Manifest.permission.WRITE_THEMES,
+ getCallingPackage())) {
+ mThemeToApply = intent.getStringExtra(EXTRA_PKGNAME);
+ }
+ } else if (ACTION_PICK_LOCK_SCREEN_WALLPAPER.equals(action)) {
+ mShowLockScreenWallpaper = true;
+ }
+ }
+
+ private String getSelectedTheme(String requestedTheme) {
+ String[] projection = { ThemesColumns.PRESENT_AS_THEME };
+ String selection = ThemesColumns.PKG_NAME + "=?";
+ String[] selectionArgs = { requestedTheme };
+
+ String selectedTheme = PreferenceUtils.getAppliedBaseTheme(this);
+
+ Cursor cursor = getContentResolver().query(ThemesColumns.CONTENT_URI,
+ projection, selection, selectionArgs, null);
+ if (cursor != null) {
+ if (cursor.getCount() > 0 && cursor.moveToFirst()) {
+ if (cursor.getInt(0) == 1) {
+ selectedTheme = requestedTheme;
+ }
+ }
+ cursor.close();
+ }
+ return selectedTheme;
+ }
+
+ private void setAnimatingStateAndScheduleFinish() {
+ mIsAnimating = true;
+ mContainer.setIsAnimating(true);
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ mIsAnimating = false;
+ mContainer.setIsAnimating(false);
+ if (mRestartLoaderOnCollapse) {
+ mRestartLoaderOnCollapse = false;
+ startLoader(LOADER_ID_INSTALLED_THEMES);
+ }
+ }
+ }, FINISH_ANIMATION_DELAY);
+ if (mExpanded) {
+ hideBottomActionsLayout();
+ } else {
+ showBottomActionsLayout();
+ }
+ }
+
+ private void setCustomBackground(final ImageView iv, final boolean animate) {
+ final Context context = ChooserActivity.this;
+ iv.post(new Runnable() {
+ @Override
+ public void run() {
+ Bitmap tmpBmp;
+ try {
+ tmpBmp = Utils.getRegularWallpaperBitmap(context);
+ } catch (Throwable e) {
+ Log.w(TAG, "Failed to retrieve wallpaper", e);
+ tmpBmp = null;
+ }
+ // Show the grid background if no wallpaper is set.
+ // Note: no wallpaper is actually a 1x1 pixel black bitmap
+ if (tmpBmp == null || tmpBmp.getWidth() <= 1 || tmpBmp.getHeight() <= 1) {
+ iv.setImageResource(R.drawable.bg_grid);
+ // We need to change the ScaleType to FIT_XY otherwise the background is cut
+ // off a bit at the bottom.
+ iv.setScaleType(ImageView.ScaleType.FIT_XY);
+ return;
+ }
+
+ // Since we are applying a blur, we can afford to scale the bitmap down and use a
+ // smaller blur radius.
+ Bitmap inBmp = Bitmap.createScaledBitmap(tmpBmp, tmpBmp.getWidth() / 4,
+ tmpBmp.getHeight() / 4, false);
+ Bitmap outBmp = Bitmap.createBitmap(inBmp.getWidth(), inBmp.getHeight(),
+ Bitmap.Config.ARGB_8888);
+
+ // Blur the original bitmap
+ RenderScript rs = RenderScript.create(context);
+ ScriptIntrinsicBlur theIntrinsic = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
+ Allocation tmpIn = Allocation.createFromBitmap(rs, inBmp);
+ Allocation tmpOut = Allocation.createFromBitmap(rs, outBmp);
+ theIntrinsic.setRadius(5.0f);
+ theIntrinsic.setInput(tmpIn);
+ theIntrinsic.forEach(tmpOut);
+ tmpOut.copyTo(outBmp);
+
+ // Create a bitmap drawable and use a color matrix to de-saturate the image
+ BitmapDrawable[] layers = new BitmapDrawable[2];
+ layers[0] = new BitmapDrawable(getResources(), tmpBmp);
+ layers[1] = new BitmapDrawable(getResources(), outBmp);
+ ColorMatrix cm = new ColorMatrix();
+ cm.setSaturation(0);
+ Paint p = layers[0].getPaint();
+ p.setColorFilter(new ColorMatrixColorFilter(cm));
+ p = layers[1].getPaint();
+ p.setColorFilter(new ColorMatrixColorFilter(cm));
+ TransitionDrawable d = new TransitionDrawable(layers);
+
+ // All done
+ iv.setScaleType(ImageView.ScaleType.CENTER_CROP);
+ if (!animate) {
+ iv.setImageDrawable(layers[1]);
+ } else {
+ iv.setImageDrawable(d);
+ }
+ }
+ });
+ }
+
+ /**
+ * Disable the ViewPager while a theme change is occuring
+ */
+ public void themeChangeStart() {
+ lockPager();
+ mThemeChanging = true;
+ ThemeFragment f = getCurrentFragment();
+ if (f != null) {
+ mAppliedBaseTheme = f.getThemePackageName();
+ PreferenceUtils.setAppliedBaseTheme(this, mAppliedBaseTheme);
+ }
+ }
+
+ /**
+ * Re-enable the ViewPager and update the "My theme" fragment if available
+ */
+ public void themeChangeEnd(boolean isSuccess) {
+ mThemeChanging = false;
+ ThemeFragment f = getCurrentFragment();
+ if (f != null) {
+ // We currently need to recreate the adapter in order to load
+ // the changes otherwise the adapter returns the original fragments
+ // TODO: We'll need a better way to handle this to provide a good UX
+ if (!(f instanceof MyThemeFragment)) {
+ mAdapter = new ThemesAdapter();
+ mPager.setAdapter(mAdapter);
+ }
+ if (!isSuccess) {
+ mAppliedBaseTheme = null;
+ }
+ startLoader(LOADER_ID_APPLIED);
+ }
+ unlockPager();
+ }
+
+ public void lockPager() {
+ mPager.setScrollingEnabled(false);
+ }
+
+ public void unlockPager() {
+ mPager.setScrollingEnabled(true);
+ }
+
+ public ComponentSelector getComponentSelector() {
+ return mSelector;
+ }
+
+ public void showComponentSelector(String component, View v) {
+ showComponentSelector(component, null, DEFAULT_COMPONENT_ID, v);
+ }
+
+ public void showComponentSelector(String component, String selectedPkgName,
+ long selectedCmpntId, View v) {
+ if (component != null) {
+ final Resources res = getResources();
+ int itemsPerPage = res.getInteger(R.integer.default_items_per_page);
+ int height = res.getDimensionPixelSize(R.dimen.component_selection_cell_height);
+ if (MODIFIES_BOOT_ANIM.equals(component)) {
+ itemsPerPage = res.getInteger(R.integer.bootani_items_per_page);
+ height = res.getDimensionPixelSize(
+ R.dimen.component_selection_cell_height_boot_anim);
+ } else if (MODIFIES_ALARMS.equals(component) ||
+ MODIFIES_NOTIFICATIONS.equals(component) ||
+ MODIFIES_RINGTONES.equals(component)) {
+ itemsPerPage = 2;
+ height = res.getDimensionPixelSize(
+ R.dimen.component_selection_cell_height_sounds);
+ }
+ if (mSaveApplyLayout.getVisibility() == View.VISIBLE) {
+ if (mSaveApplyLayout.getTranslationY() + height != 0) {
+ mSaveApplyLayout.animate()
+ .translationY(-height)
+ .setInterpolator(
+ new DecelerateInterpolator(
+ ANIMATE_SAVE_APPLY_DECELERATE_INTERPOLATOR_FACTOR))
+ .setDuration(ANIMATE_SAVE_APPLY_LAYOUT_DURATION);
+ }
+ }
+ mSelector.show(component, selectedPkgName, selectedCmpntId, itemsPerPage, height);
+
+ // determine if we need to shift the cards up
+ int[] coordinates = new int[2];
+ v.getLocationOnScreen(coordinates);
+ final int top = coordinates[1];
+ final int bottom = coordinates[1] + v.getHeight();
+ final int statusBarHeight = res.getDimensionPixelSize(R.dimen.status_bar_height);
+ int selectorTop = getWindowManager().getDefaultDisplay().getHeight() - height;
+ if (bottom > selectorTop) {
+ slideContentIntoView(bottom - selectorTop, height);
+ } else if (top < statusBarHeight) {
+ slideContentIntoView(top - statusBarHeight, height);
+ }
+ }
+ }
+
+ public void expand() {
+ if (!mExpanded && !mIsAnimating) {
+ mExpanded = true;
+ mContainer.setClickable(false);
+ mContainer.expand();
+ ThemeFragment f = getCurrentFragment();
+ if (f != null) {
+ f.expand();
+ }
+ setAnimatingStateAndScheduleFinish();
+ }
+ }
+
+ public void collapse(final boolean applyTheme) {
+ mExpanded = false;
+ final ThemeFragment f = getCurrentFragment();
+ if (f != null) {
+ f.fadeOutCards(new Runnable() {
+ public void run() {
+ mContainer.collapse();
+ f.collapse(applyTheme);
+ }
+ });
+ }
+ setAnimatingStateAndScheduleFinish();
+ }
+
+ public void pickExternalWallpaper() {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType(TYPE_IMAGE);
+ startActivityForResult(intent, REQUEST_PICK_WALLPAPER_IMAGE);
+ mIsPickingImage = true;
+ }
+
+ public void pickExternalLockscreen() {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType(TYPE_IMAGE);
+ startActivityForResult(intent, REQUEST_PICK_LOCKSCREEN_IMAGE);
+ mIsPickingImage = true;
+ }
+
+ public void uninstallTheme(String pkgName) {
+ PackageManager pm = getPackageManager();
+ pm.deletePackage(pkgName, new PackageDeleteObserver(), PackageManager.DELETE_ALL_USERS);
+ }
+
+ private void slideContentIntoView(int yDelta, int selectorHeight) {
+ ThemeFragment f = getCurrentFragment();
+ if (f != null) {
+ final int offset = getResources().getDimensionPixelSize(R.dimen.content_offset_padding);
+ if (yDelta > 0) {
+ yDelta += offset;
+ } else {
+ yDelta -= offset;
+ }
+ f.slideContentIntoView(yDelta, selectorHeight);
+ mContainerYOffset = yDelta;
+ }
+ }
+
+ private void slideContentBack(final int yDelta) {
+ ThemeFragment f = getCurrentFragment();
+ if (f != null) {
+ f.slideContentBack(yDelta);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ setCustomBackground(mCustomBackground, mAnimateContentIn);
+ // clear out any notifications that are being displayed.
+ NotificationHelper.cancelNotifications(this);
+
+ ThemeManager tm = ThemeManager.getInstance(this);
+ mThemeChanging = tm.isThemeApplying();
+
+ if (!mIsPickingImage) {
+ startLoader(LOADER_ID_APPLIED);
+ } else {
+ mIsPickingImage = false;
+ }
+
+ IntentFilter filter = new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED);
+ registerReceiver(mWallpaperChangeReceiver, filter);
+ }
+
+ @Override
+ public void onBackPressed() {
+ final ThemeFragment f = getCurrentFragment();
+ if (mSelector.isEnabled()) {
+ mSelector.hide();
+ if (mContainerYOffset != 0) {
+ slideContentBack(-mContainerYOffset);
+ mContainerYOffset = 0;
+ }
+ if (f != null) f.fadeInCards();
+ if (mShowLockScreenWallpaper) {
+ mShowLockScreenWallpaper = false;
+ mSelector.resetComponentType();
+ }
+ } else if (mExpanded) {
+ if (mIsAnimating) {
+ return;
+ }
+
+ if (mSaveApplyLayout.getVisibility() == View.VISIBLE) {
+ hideSaveApplyButton();
+ if (f != null) f.clearChanges();
+ }
+ collapse(false);
+ } else {
+ if (f != null && f.isShowingConfirmCancelOverlay()) {
+ f.hideConfirmCancelOverlay();
+ } else if (f != null && f.isShowingCustomizeResetLayout()) {
+ f.hideCustomizeResetLayout();
+ } else {
+ super.onBackPressed();
+ }
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ unregisterReceiver(mWallpaperChangeReceiver);
+ ThemeFragment f = getCurrentFragment();
+ if (f != null) {
+ mSelectedTheme = f.getThemePackageName();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (mTypefaceHelperCache.getTypefaceCount() <= 0) {
+ new TypefacePreloadTask().execute();
+ }
+ mAnimateContentInDelay = ANIMATE_CONTENT_DELAY;
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == RESULT_OK && requestCode == REQUEST_PICK_WALLPAPER_IMAGE) {
+ if (data != null && data.getData() != null) {
+ Uri uri = data.getData();
+ ThemeFragment f = getCurrentFragment();
+ if (f != null) {
+ f.setWallpaperImageUri(uri);
+ showSaveApplyButton();
+ }
+ }
+ } else if (resultCode == RESULT_OK && requestCode == REQUEST_PICK_LOCKSCREEN_IMAGE) {
+ if (data != null && data.getData() != null) {
+ Uri uri = data.getData();
+ ThemeFragment f = getCurrentFragment();
+ if (f != null) {
+ f.setLockscreenImageUri(uri);
+ showSaveApplyButton();
+ }
+ }
+ } else if (requestCode == REQUEST_SYSTEM_WINDOW_PERMISSION) {
+ if (Settings.canDrawOverlays(this)) {
+ launchAppThemer();
+ }
+ }
+ }
+
+ private void animateContentIn() {
+ Drawable d = mCustomBackground.getDrawable();
+ if (d instanceof TransitionDrawable) {
+ ((TransitionDrawable) d).startTransition((int) ANIMATE_CONTENT_IN_BLUR_DURATION);
+ }
+
+ if (!mShowLockScreenWallpaper) {
+ AnimatorSet set = new AnimatorSet();
+ set.play(ObjectAnimator.ofFloat(mContainer, "alpha", 0f, 1f)
+ .setDuration(ANIMATE_CONTENT_IN_ALPHA_DURATION))
+ .with(ObjectAnimator.ofFloat(mContainer, "scaleX", 2f, 1f)
+ .setDuration(ANIMATE_CONTENT_IN_SCALE_DURATION))
+ .with(ObjectAnimator.ofFloat(mContainer, "scaleY", 2f, 1f)
+ .setDuration(ANIMATE_CONTENT_IN_SCALE_DURATION));
+ set.setStartDelay(mAnimateContentInDelay);
+ set.start();
+ mBottomActionsLayout.setAlpha(0f);
+ mBottomActionsLayout.animate().alpha(1f).setStartDelay(mAnimateContentInDelay)
+ .setDuration(ANIMATE_CONTENT_IN_ALPHA_DURATION);
+ } else {
+ mContainer.setAlpha(0f);
+ mContainer.setVisibility(View.GONE);
+ }
+ mAnimateContentIn = false;
+ }
+
+ private View.OnClickListener mPagerClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ThemeFragment f = getCurrentFragment();
+ if (f != null && !mThemeChanging) {
+ f.performClick(mPager.isClickedOnContent());
+ }
+ }
+ };
+
+ private BroadcastReceiver mWallpaperChangeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mCustomBackground != null) setCustomBackground(mCustomBackground, false);
+ }
+ };
+
+ private ComponentSelector.OnOpenCloseListener mOpenCloseListener = new ComponentSelector.OnOpenCloseListener() {
+ @Override
+ public void onSelectorOpened() {
+ }
+
+ @Override
+ public void onSelectorClosed() {
+ }
+
+ @Override
+ public void onSelectorClosing() {
+ ThemeFragment f = getCurrentFragment();
+ if (f != null && f.componentsChanged()
+ && mSaveApplyLayout.getVisibility() == View.VISIBLE) {
+ mSaveApplyLayout.animate()
+ .translationY(0)
+ .setInterpolator(new DecelerateInterpolator())
+ .setDuration(ANIMATE_SAVE_APPLY_LAYOUT_DURATION);
+ }
+ }
+ };
+
+ private ThemeFragment getCurrentFragment() {
+ // instantiateItem will return the fragment if it already exists and not instantiate it,
+ // which should be the case for the current fragment.
+ ThemeFragment f;
+ try {
+ f = (mAdapter == null || mPager == null || mAdapter.getCount() <= 0) ? null :
+ (ThemeFragment) mAdapter.instantiateItem(mPager, mPager.getCurrentItem());
+ } catch (Exception e) {
+ f = null;
+ Log.e(TAG, "Unable to get current fragment", e);
+ }
+ return f;
+ }
+
+ private void populateCurrentTheme(Cursor c) {
+ c.moveToPosition(-1);
+ //Default to first wallpaper
+ mCurrentWallpaperCmpntId.value = DEFAULT_COMPONENT_ID;
+ // clear out the previous map
+ mCurrentTheme.clear();
+ while(c.moveToNext()) {
+ int mixkeyIdx = c.getColumnIndex(ThemesContract.MixnMatchColumns.COL_KEY);
+ int pkgIdx = c.getColumnIndex(ThemesContract.MixnMatchColumns.COL_VALUE);
+ int cmpntIdIdx = c.getColumnIndex(ThemesContract.MixnMatchColumns.COL_COMPONENT_ID);
+ String mixkey = c.getString(mixkeyIdx);
+ String component = ThemesContract.MixnMatchColumns.mixNMatchKeyToComponent(mixkey);
+ String pkg = c.getString(pkgIdx);
+ mCurrentTheme.put(component, pkg);
+ if (TextUtils.equals(component, ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN)) {
+ mCurrentTheme.remove(ThemesColumns.MODIFIES_LOCKSCREEN);
+ }
+ if (TextUtils.equals(component, ThemesColumns.MODIFIES_LOCKSCREEN)) {
+ mCurrentTheme.remove(ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN);
+ }
+ if (cmpntIdIdx >= 0 && TextUtils.equals(component, ThemesColumns.MODIFIES_LAUNCHER)) {
+ mCurrentWallpaperCmpntId.value = c.getLong(cmpntIdIdx);
+ }
+ }
+ }
+
+ private View.OnClickListener mOnShopThemesClicked = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent();
+ intent.setClassName(THEME_STORE_PACKAGE, THEME_STORE_ACTIVITY);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ try {
+ startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ lauchGetThemes();
+ }
+ }
+ };
+
+ private <T> void startLoader(int loaderId) {
+ final LoaderManager manager = getSupportLoaderManager();
+ final Loader<T> loader = manager.getLoader(loaderId);
+ if (loader != null) {
+ manager.restartLoader(loaderId, null, this);
+ } else {
+ manager.initLoader(loaderId, null, this);
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (mThemeChanging) return;
+
+ if (mExpanded && !mActivityResuming) {
+ mRestartLoaderOnCollapse = true;
+ return;
+ }
+
+ switch (loader.getId()) {
+ case LOADER_ID_INSTALLED_THEMES:
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ int selectedThemeIndex = -1;
+ if (TextUtils.isEmpty(mSelectedTheme)) mSelectedTheme = mAppliedBaseTheme;
+ while(data.moveToNext()) {
+ if (mSelectedTheme.equals(data.getString(
+ data.getColumnIndex(ThemesColumns.PKG_NAME)))) {
+ // we need to add one here since the first card is "My theme"
+ selectedThemeIndex = data.getPosition();
+ mSelectedTheme = null;
+ break;
+ }
+ }
+ data.moveToFirst();
+ mAdapter.swapCursor(data);
+ mAdapter.notifyDataSetChanged();
+ if (selectedThemeIndex >= 0) {
+ mPager.setCurrentItem(selectedThemeIndex, false);
+
+ if (mThemeToApply != null) {
+ ThemeFragment f = getCurrentFragment();
+ f.applyThemeWhenPopulated(mThemeToApply, mComponentsToApply);
+ mThemeToApply = null;
+ }
+ }
+ if (mAnimateContentIn) animateContentIn();
+ mActivityResuming = true;
+ break;
+ case LOADER_ID_APPLIED:
+ startLoader(LOADER_ID_INSTALLED_THEMES);
+ populateCurrentTheme(data);
+ break;
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ switch (loader.getId()) {
+ case LOADER_ID_INSTALLED_THEMES:
+ mAdapter.swapCursor(null);
+ mAdapter.notifyDataSetChanged();
+ break;
+ }
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_ID_INSTALLED_THEMES:
+ mAppliedBaseTheme = PreferenceUtils.getAppliedBaseTheme(this);
+ break;
+ case LOADER_ID_APPLIED:
+ //TODO: Mix n match query should only be done once
+ break;
+ }
+ return CursorLoaderHelper.chooserActivityCursorLoader(this, id, mAppliedBaseTheme);
+ }
+
+ public Map<String, String> getSelectedComponentsMap() {
+ return getCurrentFragment().getSelectedComponentsMap();
+ }
+
+ public class ThemesAdapter extends NewFragmentStatePagerAdapter {
+ private ArrayList<String> mInstalledThemes;
+ private String mAppliedThemeTitle;
+ private String mAppliedThemeAuthor;
+ private HashMap<String, Integer> mRepositionedFragments;
+
+ public ThemesAdapter() {
+ super(getSupportFragmentManager());
+ mRepositionedFragments = new HashMap<String, Integer>();
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ ThemeFragment f = null;
+ MutableLong wallpaperCmpntId;
+ if (mInstalledThemes != null) {
+ final String pkgName = mInstalledThemes.get(position);
+ if (pkgName.equals(mAppliedBaseTheme)) {
+ f = MyThemeFragment.newInstance(mAppliedBaseTheme, mAppliedThemeTitle,
+ mAppliedThemeAuthor, mAnimateContentIn, mShowLockScreenWallpaper);
+ wallpaperCmpntId = mCurrentWallpaperCmpntId;
+ } else {
+ f = ThemeFragment.newInstance(pkgName, mAnimateContentIn);
+ wallpaperCmpntId = new MutableLong(DEFAULT_COMPONENT_ID);
+ }
+ f.setCurrentTheme(mCurrentTheme, wallpaperCmpntId);
+ }
+ return f;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (mInstalledThemes != null) {
+ final String pkgName = mInstalledThemes.get(position);
+ return pkgName.hashCode();
+ }
+ return 0;
+ }
+
+ @Override
+ public int getItemPosition(Object object) {
+ final ThemeFragment f = (ThemeFragment) object;
+ final String pkgName = f != null ? f.getThemePackageName() : null;
+ if (pkgName != null && mRepositionedFragments.containsKey(pkgName)) {
+ final int position = mRepositionedFragments.get(pkgName);
+ mRepositionedFragments.remove(pkgName);
+ return position;
+ }
+ return super.getItemPosition(object);
+ }
+
+ /**
+ * The first card should be the user's currently applied theme components so we
+ * will always return at least 1 or mCursor.getCount() + 1
+ * @return
+ */
+ public int getCount() {
+ return mInstalledThemes == null ? 0 : mInstalledThemes.size();
+ }
+
+ public void swapCursor(Cursor c) {
+ if (c != null && c.getCount() != 0) {
+ ArrayList<String> previousOrder = mInstalledThemes == null ? null
+ : new ArrayList<String>(mInstalledThemes);
+ mInstalledThemes = new ArrayList<String>(c.getCount());
+ mRepositionedFragments.clear();
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ final int pkgIdx = c.getColumnIndex(ThemesColumns.PKG_NAME);
+ final String pkgName = c.getString(pkgIdx);
+ if (pkgName.equals(mAppliedBaseTheme)) {
+ final int titleIdx = c.getColumnIndex(ThemesColumns.TITLE);
+ final int authorIdx = c.getColumnIndex(ThemesColumns.AUTHOR);
+ mAppliedThemeTitle = c.getString(titleIdx);
+ mAppliedThemeAuthor = c.getString(authorIdx);
+ }
+ mInstalledThemes.add(pkgName);
+
+ // track any themes that may have changed position
+ if (previousOrder != null && previousOrder.contains(pkgName)) {
+ int index = previousOrder.indexOf(pkgName);
+ if (index != c.getPosition()) {
+ mRepositionedFragments.put(pkgName, c.getPosition());
+ }
+ } else {
+ mRepositionedFragments.put(pkgName, c.getPosition());
+ }
+ }
+ // check if any themes are no longer in the new list
+ if (previousOrder != null) {
+ for (String pkgName : previousOrder) {
+ if (!mInstalledThemes.contains(pkgName)) {
+ mRepositionedFragments.put(pkgName, POSITION_NONE);
+ }
+ }
+ }
+ } else {
+ mInstalledThemes = null;
+ }
+ }
+
+ public void removeTheme(String pkgName) {
+ if (mInstalledThemes == null) return;
+
+ if (mInstalledThemes.contains(pkgName)) {
+ final int count = mInstalledThemes.size();
+ final int index = mInstalledThemes.indexOf(pkgName);
+ // reposition all the fragments after the one being removed
+ for (int i = index + 1; i < count; i++) {
+ mRepositionedFragments.put(mInstalledThemes.get(i), i - 1);
+ }
+ // Now remove this theme and add it to mRepositionedFragments with POSITION_NONE
+ mInstalledThemes.remove(pkgName);
+ mRepositionedFragments.put(pkgName, POSITION_NONE);
+ // now we can call notifyDataSetChanged()
+ notifyDataSetChanged();
+ }
+ }
+ }
+
+ private class TypefacePreloadTask extends AsyncTask {
+
+ @Override
+ protected Object doInBackground(Object[] params) {
+ String[] projection = { ThemesColumns.PKG_NAME };
+ String selection = ThemesColumns.MODIFIES_FONTS + "=?";
+ String[] selectionArgs = { "1" };
+ Cursor c = getContentResolver().query(ThemesColumns.CONTENT_URI, projection, selection,
+ selectionArgs, null);
+ if (c != null) {
+ while (c.moveToNext()) {
+ mTypefaceHelperCache.getHelperForTheme(ChooserActivity.this, c.getString(0));
+ }
+ c.close();
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Internal delete callback from the system
+ */
+ class PackageDeleteObserver extends IPackageDeleteObserver.Stub {
+ public void packageDeleted(final String packageName, int returnCode) throws RemoteException {
+ if (returnCode == PackageManager.DELETE_SUCCEEDED) {
+ Log.d(TAG, "Delete succeeded");
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mAdapter.removeTheme(packageName);
+ }
+ });
+ } else {
+ Log.e(TAG, "Delete failed with returnCode " + returnCode);
+ }
+ }
+ }
+
+ public void expandContentAndAnimateLockScreenCardIn() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ expand();
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ AnimatorSet set = new AnimatorSet();
+ set.play(ObjectAnimator.ofFloat(mContainer, "alpha", 0f, 1f)
+ .setDuration(ANIMATE_CARDS_IN_DURATION));
+ set.setStartDelay(mAnimateContentInDelay);
+ set.start();
+ mContainer.setVisibility(View.VISIBLE);
+ getCurrentFragment().showLockScreenCard();
+ }
+ }, ANIMATE_CARDS_IN_DURATION);
+ }
+ });
+ }
+
+ private void launchAppThemer() {
+ PreferenceUtils.setShowPerAppThemeNewTag(ChooserActivity.this, false);
+ Intent intent = new Intent(ChooserActivity.this, PerAppThemingWindow.class);
+ startService(intent);
+ finish();
+ }
+
+ private void requestSystemWindowPermission() {
+ Intent intent = new Intent (Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+ Uri.parse("package:" + getPackageName()));
+ startActivityForResult(intent, REQUEST_SYSTEM_WINDOW_PERMISSION);
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/ComponentCardView.java b/src/org/cyanogenmod/theme/chooser/ComponentCardView.java
new file mode 100644
index 0000000..54d97c9
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/ComponentCardView.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.IntEvaluator;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOverlay;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class ComponentCardView extends LinearLayout {
+ public static final int CARD_FADE_DURATION = 300;
+
+ private static final float SEMI_OPAQUE_ALPHA = 0.4f;
+
+ protected TextView mLabel;
+ protected View mEmptyView;
+ protected View mContentView;
+
+ // Expanded Padding
+ int mExpandPadLeft;
+ int mExpandPadTop;
+ int mExpandPadRight;
+ int mExpandPadBottom;
+
+ // The background drawable is returning an alpha of 0 regardless of what we set it to
+ // so this will help us keep track of what the drawable's alpha is at.
+ private int mBackgroundAlpha = 255;
+
+ public ComponentCardView(Context context) {
+ this(context, null);
+ }
+
+ public ComponentCardView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ComponentCardView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setEnabled(false);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ mLabel = (TextView) findViewById(R.id.label);
+
+ Resources r = getContext().getResources();
+ mExpandPadLeft =
+ (int) r.getDimension(R.dimen.card_padding_left_right) + getPaddingLeft();
+ mExpandPadTop =
+ (int) r.getDimension(R.dimen.card_padding_top) + getPaddingTop();
+ mExpandPadRight =
+ (int) r.getDimension(R.dimen.card_padding_left_right) + getPaddingRight();
+ mExpandPadBottom =
+ (int) r.getDimension(R.dimen.card_padding_bottom) + getPaddingBottom();
+ mEmptyView = findViewById(R.id.empty);
+ mContentView = findViewById(R.id.content);
+ }
+
+ public void expand(boolean showLabel) {
+ setEnabled(true);
+ TransitionDrawable bg = null;
+ if (getBackground() instanceof TransitionDrawable) {
+ bg = (TransitionDrawable) getBackground();
+ }
+ if (bg != null) {
+ Rect paddingRect = new Rect();
+ bg.getPadding(paddingRect);
+ }
+
+ setPadding(mExpandPadLeft, 0, mExpandPadRight, mExpandPadBottom);
+
+ if (mLabel != null) {
+ mLabel.setAlpha(showLabel ? 1f : 0f);
+ mLabel.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void animateExpand() {
+ if (getBackground() instanceof TransitionDrawable) {
+ TransitionDrawable background = (TransitionDrawable) getBackground();
+ if (mLabel != null) {
+ mLabel.setVisibility(View.VISIBLE);
+ mLabel.setAlpha(0f);
+ mLabel.animate().alpha(1f).setDuration(CARD_FADE_DURATION).start();
+ }
+ background.startTransition(CARD_FADE_DURATION);
+ }
+ }
+
+ public void collapse() {
+ setEnabled(false);
+ if (mLabel != null) {
+ mLabel.setVisibility(View.GONE);
+ }
+ setPadding(0, 0, 0, 0);
+ }
+
+ public void animateFadeOut() {
+ if (mLabel != null) {
+ mLabel.animate().alpha(0f).setDuration(CARD_FADE_DURATION);
+ }
+
+ if (getBackground() instanceof TransitionDrawable) {
+ TransitionDrawable background = (TransitionDrawable) getBackground();
+ background.reverseTransition(CARD_FADE_DURATION);
+ }
+ }
+
+ /**
+ * Animates the card background and the title to 20% opacity.
+ */
+ public void animateCardFadeOut() {
+ this.animate().alpha(SEMI_OPAQUE_ALPHA).setDuration(CARD_FADE_DURATION);
+ }
+
+ /**
+ * Animates the card background and the title back to full opacity.
+ */
+ public void animateCardFadeIn() {
+ this.animate().alpha(1f).setDuration(CARD_FADE_DURATION);
+ }
+
+ /**
+ * Animates a change in the content of the card
+ * @param v View in card to animate
+ * @param overlay Drawable to animate as a ViewOverlay
+ * @param duration Duration of animation
+ */
+ public void animateContentChange(View v, final Drawable overlay, long duration) {
+ final ViewOverlay viewOverlay = this.getOverlay();
+ viewOverlay.add(overlay);
+ final int x = getRelativeLeft(v);
+ final int y = getRelativeTop(v);
+ final int width = v.getWidth();
+ final int height = v.getHeight();
+ overlay.setBounds(x, y, x + v.getWidth(), y + v.getHeight());
+
+ final ValueAnimator overlayAnimator = ValueAnimator.ofFloat(1f, 0f);
+ overlayAnimator.setDuration(duration);
+ overlayAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float value = (Float) animation.getAnimatedValue();
+ overlay.setAlpha((int) (255 * value));
+ int newWidth = (int) (value * width);
+ int newHeight = (int) (value * height);
+ int dw = (width - newWidth) / 2;
+ int dh = (height - newHeight) / 2;
+ overlay.setBounds(x + dw, y + dh, x + dw + newWidth, y + dh + newHeight);
+ }
+ });
+ overlayAnimator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Clear out the ViewOverlay now that we are done animating
+ viewOverlay.clear();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+ });
+
+ AnimatorSet set = new AnimatorSet();
+ set.play(ObjectAnimator.ofFloat(v, "alpha", 0f, 1f))
+ .with(ObjectAnimator.ofFloat(v, "scaleX", 0f, 1f))
+ .with(ObjectAnimator.ofFloat(v, "scaleY", 0f, 1f));
+ set.setDuration(duration);
+
+ set.start();
+ overlayAnimator.start();
+ }
+
+ public void setEmptyViewEnabled(boolean enabled) {
+ if (mEmptyView != null) {
+ mEmptyView.setVisibility(enabled ? View.VISIBLE : View.GONE);
+ }
+ if (mContentView != null) {
+ mContentView.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE);
+ }
+ }
+
+ public boolean isShowingEmptyView() {
+ return mEmptyView != null && mEmptyView.getVisibility() == View.VISIBLE;
+ }
+
+ private int getRelativeTop(View v) {
+ if (v.getParent() == this) {
+ return v.getTop();
+ } else {
+ return v.getTop() + ((View) v.getParent()).getTop();
+ }
+ }
+
+ private int getRelativeLeft(View v) {
+ if (v.getParent() == this) {
+ return v.getLeft();
+ } else {
+ return v.getLeft() + ((View) v.getParent()).getLeft();
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/ComponentSelector.java b/src/org/cyanogenmod/theme/chooser/ComponentSelector.java
new file mode 100644
index 0000000..ad0b68c
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/ComponentSelector.java
@@ -0,0 +1,934 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.PackageManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.media.MediaPlayer;
+import android.media.RingtoneManager;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.Loader;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.AnimationUtils;
+import android.widget.ComponentSelectorLinearLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.cyanogenmod.theme.util.AudioUtils;
+import org.cyanogenmod.theme.util.CursorLoaderHelper;
+import org.cyanogenmod.theme.util.ThemedTypefaceHelper;
+import org.cyanogenmod.theme.util.TypefaceHelperCache;
+import org.cyanogenmod.theme.util.Utils;
+
+import cyanogenmod.providers.ThemesContract;
+import cyanogenmod.providers.ThemesContract.PreviewColumns;
+import cyanogenmod.providers.ThemesContract.ThemesColumns;
+
+import java.util.Map;
+
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_ALARMS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_BOOT_ANIM;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_LAUNCHER;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_LOCKSCREEN;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_NOTIFICATIONS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_OVERLAYS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_RINGTONES;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_STATUS_BAR;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_NAVIGATION_BAR;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_ICONS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_FONTS;
+
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_STATUS_BAR;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_FONT;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_ICONS;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_WALLPAPER;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_NAVIGATION_BAR;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_LOCKSCREEN;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_STYLE;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_BOOT_ANIMATION;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_RINGTONE;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_NOTIFICATION;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_ALARM;
+
+public class ComponentSelector extends LinearLayout
+ implements LoaderManager.LoaderCallbacks<Cursor> {
+ private static final String TAG = ComponentSelector.class.getSimpleName();
+
+ public static final boolean DEBUG_SELECTOR = false;
+
+ public static final String EXTERNAL_WALLPAPER = "external";
+
+ public static final String MOD_LOCK = "mod_lock";
+
+ private static final int EXTRA_WALLPAPER_COMPONENTS = 2;
+
+ private static final int EXTRA_LOCK_SCREEN_WALLPAPER_COMPONENTS = 3;
+
+ protected static final long DEFAULT_COMPONENT_ID = 0;
+
+ private Context mContext;
+ private LayoutInflater mInflater;
+ private ComponentSelectorLinearLayout mContent;
+ private LinearLayout.LayoutParams mItemParams;
+ private LinearLayout.LayoutParams mSoundItemParams =
+ new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 0, 1.0f);
+
+ private String mComponentType;
+ private int mBatteryStyle;
+ private int mItemsPerPage;
+ private String mAppliedComponentPkgName;
+ private String mSelectedComponentPkgName;
+ private long mSelectedComponentId;
+
+ // animations for bringing selector in and out of view
+ private Animation mAnimateIn;
+ private Animation mAnimateOut;
+
+ private OnItemClickedListener mListener;
+
+ private OnOpenCloseListener mOpenCloseListener;
+
+ private MediaPlayer mMediaPlayer;
+ private ImageView mCurrentPlayPause;
+
+ private TypefaceHelperCache mTypefaceCache;
+
+ private ThemesObserver mThemesObserver;
+
+ public static final String IS_LIVE_LOCK_SCREEN_VIEW = "is_live_lock_screen_view";
+ private View mPrevLockScreenView;
+
+ public ComponentSelector(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ComponentSelector);
+ mItemsPerPage = a.getInt(R.styleable.ComponentSelector_itemsPerPage,
+ context.getResources().getInteger(R.integer.default_items_per_page));
+ a.recycle();
+
+ mContext = context;
+ mInflater = LayoutInflater.from(context);
+ // TODO: Load from settings once available
+ mBatteryStyle = 0; /*Settings.System.getInt(context.getContentResolver(),
+ Settings.System.STATUS_BAR_BATTERY_STYLE, 0);*/
+
+ mAnimateIn = AnimationUtils.loadAnimation(mContext,
+ R.anim.component_selection_animate_in);
+ mAnimateOut = AnimationUtils.loadAnimation(mContext,
+ R.anim.component_selection_animate_out);
+ mAnimateIn.setAnimationListener(new AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ if (mOpenCloseListener != null) mOpenCloseListener.onSelectorOpened();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ });
+ mAnimateOut.setAnimationListener(new AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ if (mOpenCloseListener != null) mOpenCloseListener.onSelectorClosing();
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ });
+ mMediaPlayer = new MediaPlayer();
+ mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ if (mCurrentPlayPause != null) {
+ mCurrentPlayPause.setImageResource(R.drawable.media_sound_selector_preview);
+ mCurrentPlayPause = null;
+ }
+ }
+ });
+ mTypefaceCache = TypefaceHelperCache.getInstance();
+ mThemesObserver = new ThemesObserver();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mContent = (ComponentSelectorLinearLayout) findViewById(R.id.content);
+ setEnabled(false);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mThemesObserver.register();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mThemesObserver.unregister();
+ }
+
+ @Override
+ protected void onWindowVisibilityChanged(int visibility) {
+ super.onWindowVisibilityChanged(visibility);
+ // Window visibility transitions as follows
+ // VISIBLE -> INVISIBLE -> GONE
+ // GONE -> INVISIBLE -> VISIBLE
+ if (visibility == View.GONE) {
+ mThemesObserver.unregister();
+ } else if (visibility == View.VISIBLE) {
+ mThemesObserver.register();
+ }
+ }
+
+ public void resetComponentType() {
+ mComponentType = null;
+ }
+
+ public void setComponentType(String component) {
+ setComponentType(component, null, DEFAULT_COMPONENT_ID);
+ }
+
+ public void setComponentType(String component, String selectedPkgName) {
+ setComponentType(component, null, DEFAULT_COMPONENT_ID);
+ }
+
+ public void setComponentType(String component, String selectedPkgName,
+ long selectedComponentId) {
+ mSelectedComponentPkgName = selectedPkgName;
+ mSelectedComponentId = selectedComponentId;
+ if (mComponentType == null || !mComponentType.equals(component)) {
+ mContent.removeAllViews();
+ mComponentType = component;
+ ((FragmentActivity) mContext).getSupportLoaderManager().restartLoader(
+ getLoaderIdFromComponent(component), null, this);
+ } else if (mComponentType != null) {
+ int count = mContent.getChildCount();
+ final Resources res = getResources();
+ boolean highlightTitle;
+ Map<String, String> selectedComponents = null;
+ if (mComponentType.equals(MODIFIES_LOCKSCREEN)) {
+ selectedComponents = ((ChooserActivity)mContext)
+ .getSelectedComponentsMap();
+ }
+ for (int i = 0; i < count; i++) {
+ final View child = mContent.getChildAt(i);
+ final TextView tv = (TextView) child.findViewById(R.id.title);
+ // The child itself may have the tag, or in the case of sounds its the grandchild,
+ // either way the parent of the textview will have the pkgName in its tag
+ final View viewWithTag = (View) tv.getParent();
+ final String pkgName = (String) viewWithTag.getTag(R.id.tag_key_package_name);
+ Long cmpntId = (Long) viewWithTag.getTag(R.id.tag_key_component_id);
+ if (cmpntId == null) {
+ cmpntId = DEFAULT_COMPONENT_ID;
+ }
+ Boolean isLLS = (Boolean) viewWithTag.getTag(R.id.tag_key_live_lock_screen);
+ highlightTitle = false;
+ if (selectedComponents != null && isLLS != null) {
+ if ((TextUtils.equals(selectedComponents.get(MODIFIES_LOCKSCREEN), pkgName)
+ && !isLLS) || (TextUtils.equals(selectedComponents.get(
+ MODIFIES_LIVE_LOCK_SCREEN), pkgName) && isLLS)) {
+ highlightTitle = true;
+ }
+ } else if (pkgName.equals(selectedPkgName) && cmpntId == selectedComponentId) {
+ highlightTitle = true;
+ }
+ if (highlightTitle) {
+ tv.setTextColor(res.getColor(R.color.component_selection_current_text_color));
+ } else {
+ tv.setTextColor(res.getColor(android.R.color.white));
+ }
+ }
+ }
+ }
+
+ public String getComponentType() {
+ return mComponentType;
+ }
+
+ public void setNumItemsPerPage(int itemsPerPage) {
+ if (mItemsPerPage != itemsPerPage) {
+ mItemsPerPage = itemsPerPage;
+ }
+ }
+
+ public void setHeight(int height) {
+ ViewGroup.LayoutParams params = mContent.getLayoutParams();
+ if (params.height != height) {
+ params.height = height;
+ mContent.setLayoutParams(params);
+ requestLayout();
+ }
+ }
+
+ public void show(String componentType, int itemsPerPage, int height) {
+ show(componentType, null, itemsPerPage, height);
+ }
+
+ public void show(String componentType, String selectedPkgName, int itemsPerPage, int height) {
+ show(componentType, selectedPkgName, DEFAULT_COMPONENT_ID, itemsPerPage, height);
+ }
+
+ public void show(String componentType, String selectedPkgName, long selectedComponentId,
+ int itemsPerPage, int height) {
+ setNumItemsPerPage(itemsPerPage);
+ setHeight(height);
+ setComponentType(componentType, selectedPkgName, selectedComponentId);
+ show();
+ }
+
+ public void show() {
+ if (getVisibility() == View.GONE) {
+ setEnabled(true);
+ setVisibility(View.VISIBLE);
+ startAnimation(mAnimateIn);
+ }
+ }
+
+ public void hide() {
+ if (getVisibility() == View.VISIBLE && isEnabled()) {
+ setEnabled(false);
+ startAnimation(mAnimateOut);
+ }
+ if (mMediaPlayer != null && mMediaPlayer.isPlaying()) mMediaPlayer.stop();
+ }
+
+ private int getLoaderIdFromComponent(String component) {
+ if (MODIFIES_STATUS_BAR.equals(component)) {
+ return LOADER_ID_STATUS_BAR;
+ }
+ if (MODIFIES_NAVIGATION_BAR.equals(component)) {
+ return LOADER_ID_NAVIGATION_BAR;
+ }
+ if (MODIFIES_FONTS.equals(component)) {
+ return LOADER_ID_FONT;
+ }
+ if (MODIFIES_ICONS.equals(component)) {
+ return LOADER_ID_ICONS;
+ }
+ if (MODIFIES_OVERLAYS.equals(component)) {
+ return LOADER_ID_STYLE;
+ }
+ if (MODIFIES_LAUNCHER.equals(component)) {
+ return LOADER_ID_WALLPAPER;
+ }
+ if (MODIFIES_BOOT_ANIM.equals(component)) {
+ return LOADER_ID_BOOT_ANIMATION;
+ }
+ if (MODIFIES_RINGTONES.equals(component)) {
+ return LOADER_ID_RINGTONE;
+ }
+ if (MODIFIES_NOTIFICATIONS.equals(component)) {
+ return LOADER_ID_NOTIFICATION;
+ }
+ if (MODIFIES_ALARMS.equals(component)) {
+ return LOADER_ID_ALARM;
+ }
+ if (MODIFIES_LOCKSCREEN.equals(component)) {
+ return LOADER_ID_LOCKSCREEN;
+ }
+ return -1;
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ return CursorLoaderHelper.componentSelectorCursorLoader(mContext, id);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, final Cursor data) {
+ int currentLoaderId = loader.getId();
+ int count = data.getCount();
+ int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
+ final Resources res = getResources();
+ int dividerPadding = res.getDimensionPixelSize(R.dimen.component_divider_padding_top);
+ int dividerHeight = res.getDimensionPixelSize(R.dimen.component_divider_height);
+ MatrixCursor lockScreenMatrixCursor = null;
+
+ switch (currentLoaderId) {
+ case LOADER_ID_ALARM:
+ case LOADER_ID_NOTIFICATION:
+ case LOADER_ID_RINGTONE:
+ mItemParams = new LayoutParams(screenWidth,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ // Sounds are a special case where there are two items laid out
+ // vertically. This layout is added as a single item so we need to
+ // adjust the count by dividing by the number of items per page and
+ // rounding up so we include all items.
+ count = (int) Math.ceil((double)count / mItemsPerPage);
+ mContent.setShowDividers(LinearLayout.SHOW_DIVIDER_NONE);
+ break;
+ case LOADER_ID_BOOT_ANIMATION:
+ dividerPadding = res.getDimensionPixelSize(
+ R.dimen.component_divider_padding_top_bootani);
+ dividerHeight = res.getDimensionPixelSize(R.dimen.component_divider_height_bootani);
+ // fall through to default
+ default:
+ mItemParams = new LayoutParams(screenWidth / mItemsPerPage,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ if (currentLoaderId == LOADER_ID_WALLPAPER ||
+ currentLoaderId == LOADER_ID_LOCKSCREEN) {
+ count += EXTRA_WALLPAPER_COMPONENTS;
+ }
+
+ if (currentLoaderId == LOADER_ID_LOCKSCREEN) {
+ lockScreenMatrixCursor = splitLockScreenCursor(data);
+ if (lockScreenMatrixCursor != null) {
+ count = lockScreenMatrixCursor.getCount() + EXTRA_WALLPAPER_COMPONENTS;
+ }
+ }
+
+ mContent.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
+ break;
+ }
+
+ mContent.setDividerPadding(dividerPadding);
+ mContent.setDividerHeight(dividerHeight);
+
+ new LoadItemsTask().execute((lockScreenMatrixCursor != null)
+ ? lockScreenMatrixCursor : data, count);
+ }
+
+ /* Some themes might contain a lock wallpaper AND a live lock screen.
+ * ThemesProvider will return one single row containing both thumbnail paths
+ * (ThemesProvider groups by theme_id) so we need to create a cursor on the
+ * fly to split that row into 2 to properly generate a view for each thumbnail
+ */
+ private MatrixCursor splitLockScreenCursor(Cursor data) {
+ int lockWallPaperThumbnailIndx, llsThumbnailIndx, pkgIndx;
+ String lockWallPaperThumbnail, liveLockScreenThumbnail, pkgName;
+ MatrixCursor lockScreenMatrixCursor;
+ int needToSplitRowAt = -1;
+
+ lockWallPaperThumbnailIndx = data.getColumnIndex(PreviewColumns.LOCK_WALLPAPER_THUMBNAIL);
+ llsThumbnailIndx = data.getColumnIndex(PreviewColumns.LIVE_LOCK_SCREEN_THUMBNAIL);
+ pkgIndx = data.getColumnIndex(ThemesColumns.PKG_NAME);
+
+ if (lockWallPaperThumbnailIndx < 0 || llsThumbnailIndx < 0 || pkgIndx < 0) {
+ //An invalid cursor was provided, we can't continue processing
+ Log.e(TAG, "Failed to process cursor due to missing columns");
+ return null;
+ }
+
+ //Let's find out if we really need to allocate a MatrixCursor.
+ //If we find at least one row with valid data in lock_wallpaper_thumbnail AND
+ //live_lock_screen_thumbnail it means we do.
+ data.moveToPosition(-1);
+ while (data.moveToNext()) {
+ lockWallPaperThumbnail = data.getString(lockWallPaperThumbnailIndx);
+ liveLockScreenThumbnail = data.getString(llsThumbnailIndx);
+ if (!TextUtils.isEmpty(lockWallPaperThumbnail)
+ && !TextUtils.isEmpty(liveLockScreenThumbnail)) {
+ needToSplitRowAt = data.getPosition();
+ break;
+ }
+ }
+
+ if (needToSplitRowAt == -1) return null;
+
+ lockScreenMatrixCursor = new MatrixCursor(data.getColumnNames());
+ //Clone all the *regular* rows up to needToSplitRowAt
+ for (int indx = 0; indx < needToSplitRowAt; indx++) {
+ data.moveToPosition(indx);
+ lockScreenMatrixCursor.addRow(CursorLoaderHelper.getRowFromCursor(data));
+ }
+ if (needToSplitRowAt == 0) {
+ data.moveToPosition(-1);
+ }
+ while (data.moveToNext()) {
+ lockWallPaperThumbnail = data.getString(lockWallPaperThumbnailIndx);
+ liveLockScreenThumbnail = data.getString(llsThumbnailIndx);
+ if (!TextUtils.isEmpty(lockWallPaperThumbnail)
+ && !TextUtils.isEmpty(liveLockScreenThumbnail)) {
+ pkgName = data.getString(pkgIndx);
+
+ MatrixCursor.RowBuilder lockWallpaperRow = lockScreenMatrixCursor.newRow();
+ MatrixCursor.RowBuilder liveLockScreenRow = lockScreenMatrixCursor.newRow();
+
+ for (String col : data.getColumnNames()) {
+ if (TextUtils.equals(col, PreviewColumns.LOCK_WALLPAPER_THUMBNAIL)) {
+ lockWallpaperRow.add(PreviewColumns.LOCK_WALLPAPER_THUMBNAIL,
+ lockWallPaperThumbnail);
+ liveLockScreenRow.add(PreviewColumns.LOCK_WALLPAPER_THUMBNAIL, null);
+ } else if (TextUtils.equals(col, PreviewColumns.LIVE_LOCK_SCREEN_THUMBNAIL)) {
+ lockWallpaperRow.add(PreviewColumns.LIVE_LOCK_SCREEN_THUMBNAIL, null);
+ liveLockScreenRow.add(PreviewColumns.LIVE_LOCK_SCREEN_THUMBNAIL,
+ liveLockScreenThumbnail);
+ } else if (TextUtils.equals(col, MODIFIES_LIVE_LOCK_SCREEN)) {
+ lockWallpaperRow.add(MODIFIES_LIVE_LOCK_SCREEN, 0);
+ liveLockScreenRow.add(MODIFIES_LIVE_LOCK_SCREEN, 1);
+ } else {
+ int colIndx = data.getColumnIndex(col);
+ lockWallpaperRow.add(col, CursorLoaderHelper.getFieldValueFromRow(data,
+ colIndx));
+ liveLockScreenRow.add(col, CursorLoaderHelper.getFieldValueFromRow(data,
+ colIndx));
+ }
+ }
+ } else {
+ //This is a regular row, so just clone it
+ lockScreenMatrixCursor.addRow(CursorLoaderHelper.getRowFromCursor(data));
+ }
+ }
+ return lockScreenMatrixCursor;
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ }
+
+ public void setOnItemClickedListener(OnItemClickedListener listener) {
+ mListener = listener;
+ }
+
+ public void setOnOpenCloseListener(OnOpenCloseListener listener) {
+ mOpenCloseListener = listener;
+ }
+
+ private View newView(Cursor cursor, int position, ViewGroup container) {
+ if (MODIFIES_STATUS_BAR.equals(mComponentType)) {
+ return newStatusBarView(cursor, container, position);
+ }
+ if (MODIFIES_NAVIGATION_BAR.equals(mComponentType)) {
+ return newNavBarView(cursor, container, position);
+ }
+ if (MODIFIES_FONTS.equals(mComponentType)) {
+ return newFontView(cursor, container, position);
+ }
+ if (MODIFIES_ICONS.equals(mComponentType)) {
+ return newIconView(cursor, container, position);
+ }
+ if (MODIFIES_OVERLAYS.equals(mComponentType)) {
+ return newStyleView(cursor, container, position);
+ }
+ if (MODIFIES_LAUNCHER.equals(mComponentType)) {
+ return newWallpapersView(cursor, container, position,
+ cursor.getColumnIndex(PreviewColumns.WALLPAPER_THUMBNAIL), false,
+ EXTRA_WALLPAPER_COMPONENTS);
+ }
+ if (MODIFIES_BOOT_ANIM.equals(mComponentType)) {
+ return newBootanimationView(cursor, container, position);
+ }
+ if (MODIFIES_RINGTONES.equals(mComponentType) ||
+ MODIFIES_NOTIFICATIONS.equals(mComponentType) ||
+ MODIFIES_ALARMS.equals(mComponentType)) {
+ return newSoundView(cursor, container, position, mComponentType);
+ }
+ if (MODIFIES_LOCKSCREEN.equals(mComponentType)) {
+ boolean isLiveLockScreen = false;
+ if (position >= EXTRA_LOCK_SCREEN_WALLPAPER_COMPONENTS) {
+ cursor.moveToPosition(position - EXTRA_LOCK_SCREEN_WALLPAPER_COMPONENTS);
+ int liveLockIndex = cursor.getColumnIndex(MODIFIES_LIVE_LOCK_SCREEN);
+ isLiveLockScreen = liveLockIndex >= 0 &&
+ cursor.getInt(liveLockIndex) == 1;
+ }
+ int index = isLiveLockScreen
+ ? cursor.getColumnIndex(PreviewColumns.LIVE_LOCK_SCREEN_THUMBNAIL)
+ : cursor.getColumnIndex(PreviewColumns.LOCK_WALLPAPER_THUMBNAIL);
+ return newWallpapersView(cursor, container, position, index, isLiveLockScreen,
+ EXTRA_LOCK_SCREEN_WALLPAPER_COMPONENTS);
+ }
+ return null;
+ }
+
+ private View newStatusBarView(Cursor cursor, ViewGroup parent, int position) {
+ cursor.moveToPosition(position);
+ View v = mInflater.inflate(R.layout.status_bar_component_selection_item,
+ parent, false);
+ int wifiIndex = cursor.getColumnIndex(PreviewColumns.STATUSBAR_WIFI_ICON);
+ int signalIndex = cursor.getColumnIndex(PreviewColumns.STATUSBAR_SIGNAL_ICON);
+ int bluetoothIndex = cursor.getColumnIndex(PreviewColumns.STATUSBAR_BLUETOOTH_ICON);
+ int batteryIndex = cursor.getColumnIndex(Utils.getBatteryIndex(mBatteryStyle));
+ int backgroundIndex = cursor.getColumnIndex(PreviewColumns.STATUSBAR_BACKGROUND);
+ int pkgNameIndex = cursor.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME);
+
+ ((ImageView) v.findViewById(R.id.slot1)).setImageBitmap(
+ Utils.loadBitmapBlob(cursor, wifiIndex));
+ ((ImageView) v.findViewById(R.id.slot2)).setImageBitmap(
+ Utils.loadBitmapBlob(cursor, signalIndex));
+ ((ImageView) v.findViewById(R.id.slot3)).setImageBitmap(
+ Utils.loadBitmapBlob(cursor, bluetoothIndex));
+ ((ImageView) v.findViewById(R.id.slot4)).setImageBitmap(
+ Utils.loadBitmapBlob(cursor, batteryIndex));
+ setTitle(((TextView) v.findViewById(R.id.title)), cursor);
+ v.findViewById(R.id.container).setBackground(
+ new BitmapDrawable(Utils.loadBitmapBlob(cursor, backgroundIndex)));
+ v.setTag(R.id.tag_key_package_name, cursor.getString(pkgNameIndex));
+ v.setOnClickListener(mItemClickListener);
+ return v;
+ }
+
+ private View newNavBarView(Cursor cursor, ViewGroup parent, int position) {
+ cursor.moveToPosition(position);
+ View v = mInflater.inflate(R.layout.navigation_bar_component_selection_item, parent,
+ false);
+ int backIndex = cursor.getColumnIndex(PreviewColumns.NAVBAR_BACK_BUTTON);
+ int backgroundIndex = cursor.getColumnIndex(PreviewColumns.STATUSBAR_BACKGROUND);
+ int pkgNameIndex = cursor.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME);
+
+ ((ImageView) v.findViewById(R.id.back)).setImageBitmap(
+ Utils.loadBitmapBlob(cursor, backIndex));
+ setTitle(((TextView) v.findViewById(R.id.title)), cursor);
+ v.findViewById(R.id.container).setBackground(
+ new BitmapDrawable(Utils.loadBitmapBlob(cursor, backgroundIndex)));
+ v.setTag(R.id.tag_key_package_name, cursor.getString(pkgNameIndex));
+ v.setOnClickListener(mItemClickListener);
+ return v;
+ }
+
+ private View newFontView(Cursor cursor, ViewGroup parent, int position) {
+ cursor.moveToPosition(position);
+ View v = mInflater.inflate(R.layout.font_component_selection_item, parent, false);
+ int pkgNameIndex = cursor.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME);
+
+ TextView preview = (TextView) v.findViewById(R.id.text_preview);
+ String pkgName = cursor.getString(pkgNameIndex);
+
+ ThemedTypefaceHelper helper = mTypefaceCache.getHelperForTheme(mContext, pkgName);
+ Typeface typefaceNormal = helper.getTypeface(Typeface.NORMAL);
+ preview.setTypeface(typefaceNormal);
+
+ setTitle(((TextView) v.findViewById(R.id.title)), cursor);
+ v.setTag(R.id.tag_key_package_name, cursor.getString(pkgNameIndex));
+ v.setOnClickListener(mItemClickListener);
+ return v;
+ }
+
+ private View newIconView(Cursor cursor, ViewGroup parent, int position) {
+ cursor.moveToPosition(position);
+ View v = mInflater.inflate(R.layout.icon_component_selection_item, parent,
+ false);
+ int iconIndex = cursor.getColumnIndex(PreviewColumns.ICON_PREVIEW_1);
+ int pkgNameIndex = cursor.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME);
+
+ ((ImageView) v.findViewById(R.id.icon)).setImageBitmap(
+ Utils.loadBitmapBlob(cursor, iconIndex));
+ setTitle(((TextView) v.findViewById(R.id.title)), cursor);
+ v.setTag(R.id.tag_key_package_name, cursor.getString(pkgNameIndex));
+ v.setOnClickListener(mItemClickListener);
+ return v;
+ }
+
+ private View newStyleView(Cursor cursor, ViewGroup parent, int position) {
+ cursor.moveToPosition(position);
+ View v = mInflater.inflate(R.layout.icon_component_selection_item, parent,
+ false);
+ int styleIndex = cursor.getColumnIndex(PreviewColumns.STYLE_THUMBNAIL);
+ int pkgNameIndex = cursor.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME);
+
+ ((ImageView) v.findViewById(R.id.icon)).setImageBitmap(
+ Utils.loadBitmapBlob(cursor, styleIndex));
+ setTitle(((TextView) v.findViewById(R.id.title)), cursor);
+ v.setTag(R.id.tag_key_package_name, cursor.getString(pkgNameIndex));
+ v.setOnClickListener(mItemClickListener);
+ return v;
+ }
+
+ private View newWallpapersView(Cursor cursor, ViewGroup parent, int position,
+ int wallpaperIndex, boolean isLiveLockScreen, int numExtraComponents) {
+ View v = mInflater.inflate(R.layout.wallpaper_component_selection_item, parent,
+ false);
+ ImageView iv = (ImageView) v.findViewById(R.id.icon);
+ if (position == 0) {
+ iv.setImageResource(R.drawable.img_wallpaper_none);
+ v.setTag(R.id.tag_key_package_name, "");
+ ((TextView) v.findViewById(R.id.title)).setText(R.string.wallpaper_none_title);
+ } else if (position == 1) {
+ iv.setImageResource(R.drawable.img_wallpaper_external);
+ v.setTag(R.id.tag_key_package_name, EXTERNAL_WALLPAPER);
+ ((TextView) v.findViewById(R.id.title))
+ .setText(R.string.wallpaper_external_title);
+ } else if (numExtraComponents == EXTRA_LOCK_SCREEN_WALLPAPER_COMPONENTS && position == 2) {
+ // TODO: update drawable once the asset is provided by design
+ iv.setImageResource(android.R.drawable.ic_lock_lock);
+ v.setTag(R.id.tag_key_package_name, MOD_LOCK);
+ ((TextView) v.findViewById(R.id.title))
+ .setText(R.string.mod_lock_title);
+ } else {
+ cursor.moveToPosition(position - numExtraComponents);
+ int pkgNameIndex = cursor.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME);
+ int cmpntIdIndex = cursor.getColumnIndex(PreviewColumns.COMPONENT_ID);
+ long cmpntId = (cmpntIdIndex >= 0) ?
+ cursor.getLong(cmpntIdIndex) : DEFAULT_COMPONENT_ID;
+ iv.setImageBitmap(
+ Utils.loadBitmapBlob(cursor, wallpaperIndex));
+ setTitle(((TextView) v.findViewById(R.id.title)), cursor);
+ v.setTag(R.id.tag_key_package_name, cursor.getString(pkgNameIndex));
+ v.setTag(R.id.tag_key_component_id, cmpntId);
+ v.setTag(R.id.tag_key_live_lock_screen, isLiveLockScreen);
+ v.findViewById(R.id.live_lock_screen_badge)
+ .setVisibility(isLiveLockScreen ? View.VISIBLE : View.GONE);
+ }
+ v.setOnClickListener(mItemClickListener);
+ return v;
+ }
+
+ private View newBootanimationView(Cursor cursor, ViewGroup parent, int position) {
+ cursor.moveToPosition(position);
+ View v = mInflater.inflate(R.layout.bootani_component_selection_item, parent,
+ false);
+ int wallpaperIndex = cursor.getColumnIndex(PreviewColumns.BOOTANIMATION_THUMBNAIL);
+ int pkgNameIndex = cursor.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME);
+
+ ((ImageView) v.findViewById(R.id.preview)).setImageBitmap(
+ Utils.loadBitmapBlob(cursor, wallpaperIndex));
+ setTitle(((TextView) v.findViewById(R.id.title)), cursor);
+ v.setTag(R.id.tag_key_package_name, cursor.getString(pkgNameIndex));
+ v.setOnClickListener(mItemClickListener);
+ return v;
+ }
+
+ private View newSoundView(Cursor cursor, ViewGroup parent, int position,
+ final String component) {
+ LinearLayout container = (LinearLayout) mInflater.inflate(
+ R.layout.component_selection_sounds_pager_item, parent, false);
+ container.setWeightSum(mItemsPerPage);
+ for (int i = 0; i < mItemsPerPage; i++) {
+ int index = position * mItemsPerPage + i;
+ if (cursor.getCount() <= index) continue;
+ cursor.moveToPosition(index);
+ View v = mInflater.inflate(R.layout.sound_component_selection_item, parent,
+ false);
+ final int pkgNameIndex = cursor.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME);
+
+ setTitle(((TextView) v.findViewById(R.id.title)), cursor);
+ v.setTag(R.id.tag_key_package_name, cursor.getString(pkgNameIndex));
+ v.setOnClickListener(mItemClickListener);
+ container.addView(v, mSoundItemParams);
+ final View playButton = v.findViewById(R.id.play_button);
+ playButton.setTag(cursor.getString(pkgNameIndex));
+ playButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int type;
+ String pkgName = (String) v.getTag();
+ if (component.equals(MODIFIES_RINGTONES)) {
+ type = RingtoneManager.TYPE_RINGTONE;
+ } else if (component.equals(MODIFIES_NOTIFICATIONS)) {
+ type = RingtoneManager.TYPE_NOTIFICATION;
+ } else {
+ type = RingtoneManager.TYPE_ALARM;
+ }
+ boolean shouldStop = playButton == mCurrentPlayPause;
+ try {
+ if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+ mMediaPlayer.stop();
+ if (mCurrentPlayPause != null) {
+ mCurrentPlayPause.setImageResource(
+ R.drawable.media_sound_selector_preview);
+ }
+ mCurrentPlayPause = null;
+ }
+ if (mCurrentPlayPause != playButton && !shouldStop) {
+ AudioUtils.loadThemeAudible(mContext, type, pkgName,
+ mMediaPlayer);
+ mMediaPlayer.start();
+ mCurrentPlayPause = (ImageView) playButton;
+ mCurrentPlayPause.setImageResource(
+ R.drawable.media_sound_selector_stop);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Unable to play preview sound", e);
+ }
+ }
+ });
+ }
+
+ return container;
+ }
+
+ private class LoadItemsTask extends AsyncTask<Object, Void, Void> {
+
+ @Override
+ protected Void doInBackground(Object... params) {
+ Cursor c = (Cursor) params[0];
+ int count = (Integer) params[1];
+ for (int i = 0; i < count && !isCancelled(); i++) {
+ final View v = newView(c, i, mContent);
+ mContent.post(new Runnable() {
+ @Override
+ public void run() {
+ mContent.addView(v, mItemParams);
+ }
+ });
+ }
+
+ if (c instanceof MatrixCursor) {
+ c.close();
+ }
+
+ // destroy the loader now that we are done with it
+ ComponentSelector.this.post(new Runnable() {
+ @Override
+ public void run() {
+ ((FragmentActivity)mContext).getSupportLoaderManager().destroyLoader(
+ getLoaderIdFromComponent(mComponentType));
+ }
+ });
+ return null;
+ }
+ }
+
+ private void setTitle(TextView titleView, Cursor cursor) {
+ String pkgName = cursor.getString(cursor.getColumnIndex(ThemesColumns.PKG_NAME));
+ int cmpntIdIndex = cursor.getColumnIndex(PreviewColumns.COMPONENT_ID);
+ long cmpntId = DEFAULT_COMPONENT_ID;
+ if (cmpntIdIndex >= 0) {
+ cmpntId = cursor.getLong(cmpntIdIndex);
+ }
+ if (Utils.getDefaultThemePackageName(mContext).equals(pkgName)) {
+ titleView.setText(mContext.getString(R.string.default_tag_text));
+ titleView.setTypeface(titleView.getTypeface(), Typeface.BOLD);
+ } else {
+ titleView.setText(cursor.getString(cursor.getColumnIndex(ThemesColumns.TITLE)));
+ }
+ boolean highlightTitle = false;
+ if (mComponentType.equals(MODIFIES_LOCKSCREEN)) {
+ Map<String, String> selectedComponents = ((ChooserActivity)mContext)
+ .getSelectedComponentsMap();
+ int isLLS = cursor.getInt(cursor.getColumnIndex(MODIFIES_LIVE_LOCK_SCREEN));
+ if ((TextUtils.equals(selectedComponents.get(MODIFIES_LOCKSCREEN), pkgName)
+ && isLLS == 0) || (TextUtils.equals(
+ selectedComponents.get(MODIFIES_LIVE_LOCK_SCREEN), pkgName) && isLLS == 1)) {
+ highlightTitle = true;
+ }
+ } else if (pkgName.equals(mSelectedComponentPkgName) && cmpntId == mSelectedComponentId) {
+ highlightTitle = true;
+ }
+ if (highlightTitle) {
+ titleView.setTextColor(getResources().getColor(
+ R.color.component_selection_current_text_color));
+ }
+ }
+
+ private OnClickListener mItemClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ long cmpntId = DEFAULT_COMPONENT_ID;
+ String pkgName = (String) v.getTag(R.id.tag_key_package_name);
+ Long cmpntIdTag = (Long) v.getTag(R.id.tag_key_component_id);
+ if (cmpntIdTag != null) {
+ cmpntId = cmpntIdTag;
+ }
+ Boolean isLiveLock = (Boolean) v.getTag(R.id.tag_key_live_lock_screen);
+ boolean isSamePkgButDifferentLockScreen = false;
+ Bundle params = null;
+ if (isLiveLock != null) {
+ params = new Bundle();
+ params.putBoolean(IS_LIVE_LOCK_SCREEN_VIEW, isLiveLock);
+
+ if (pkgName.equals(mSelectedComponentPkgName) && v != mPrevLockScreenView) {
+ isSamePkgButDifferentLockScreen = true;
+ }
+ mPrevLockScreenView = v;
+ }
+ if (DEBUG_SELECTOR) Toast.makeText(mContext, pkgName, Toast.LENGTH_SHORT).show();
+ if (mListener != null && (isSamePkgButDifferentLockScreen ||
+ !pkgName.equals(mSelectedComponentPkgName) ||
+ pkgName.equals(EXTERNAL_WALLPAPER) || cmpntId != mSelectedComponentId)) {
+ mSelectedComponentPkgName = pkgName;
+ mSelectedComponentId = cmpntId;
+ mListener.onItemClicked(pkgName, cmpntId, params);
+ final int count = mContent.getChildCount();
+ final Resources res = getResources();
+ for (int i = 0; i < count; i++) {
+ final View child = mContent.getChildAt(i);
+ final TextView tv = (TextView) child.findViewById(R.id.title);
+ if (tv != null) {
+ if (child == v) {
+ tv.setTextColor(
+ res.getColor(R.color.component_selection_current_text_color));
+ } else {
+ tv.setTextColor(res.getColor(android.R.color.white));
+ }
+ }
+ }
+ }
+ }
+ };
+
+ private class ThemesObserver extends ContentObserver {
+ public ThemesObserver() {
+ super(null);
+ }
+
+ public void register() {
+ mContext.getContentResolver().registerContentObserver(
+ ThemesColumns.CONTENT_URI, false, this);
+ }
+
+ public void unregister() {
+ mContext.getContentResolver().unregisterContentObserver(this);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ // reload items by calling setComponentType()
+ if (mComponentType != null) {
+ final String componentType = mComponentType;
+ mComponentType = null;
+ mContent.post(new Runnable() {
+ @Override
+ public void run() {
+ setComponentType(componentType, mSelectedComponentPkgName);
+ }
+ });
+ }
+ }
+ }
+
+ public interface OnItemClickedListener {
+ public void onItemClicked(String pkgName, long componentId, Bundle params);
+ }
+
+ public interface OnOpenCloseListener {
+ public void onSelectorOpened();
+ public void onSelectorClosed();
+ public void onSelectorClosing();
+ }
+} \ No newline at end of file
diff --git a/src/org/cyanogenmod/theme/chooser/IconTransitionDrawable.java b/src/org/cyanogenmod/theme/chooser/IconTransitionDrawable.java
new file mode 100644
index 0000000..96547f3
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/IconTransitionDrawable.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.Canvas;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.SystemClock;
+
+/**
+ * An extension of LayerDrawables that is intended to cross-fade between
+ * the first and second layer. To start the transition, call {@link #startTransition(int)}. To
+ * display just the first layer, call {@link #resetTransition()}.
+ * <p>
+ * It can be defined in an XML file with the <code>&lt;transition></code> element.
+ * Each Drawable in the transition is defined in a nested <code>&lt;item></code>. For more
+ * information, see the guide to <a
+ * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
+ *
+ * @attr ref android.R.styleable#LayerDrawableItem_left
+ * @attr ref android.R.styleable#LayerDrawableItem_top
+ * @attr ref android.R.styleable#LayerDrawableItem_right
+ * @attr ref android.R.styleable#LayerDrawableItem_bottom
+ * @attr ref android.R.styleable#LayerDrawableItem_drawable
+ * @attr ref android.R.styleable#LayerDrawableItem_id
+ *
+ */
+public class IconTransitionDrawable extends LayerDrawable {
+
+ /**
+ * A transition is about to start.
+ */
+ private static final int TRANSITION_STARTING = 0;
+
+ /**
+ * The transition has started and the animation is in progress
+ */
+ private static final int TRANSITION_RUNNING = 1;
+
+ /**
+ * No transition will be applied
+ */
+ private static final int TRANSITION_NONE = 2;
+
+ /**
+ * The current state of the transition. One of {@link #TRANSITION_STARTING},
+ * {@link #TRANSITION_RUNNING} and {@link #TRANSITION_NONE}
+ */
+ private int mTransitionState = TRANSITION_NONE;
+
+ private long mStartTimeMillis;
+ private int mFrom;
+ private int mTo;
+ private int mDuration;
+ private int mAlpha = 0;
+ private float mFromScale;
+ private float mToScale;
+
+ /**
+ * Create a new transition drawable with the specified list of layers. At least
+ * 2 layers are required for this drawable to work properly.
+ */
+ public IconTransitionDrawable(Drawable[] layers) {
+ super(layers);
+ }
+
+ /**
+ * Begin the second layer on top of the first layer.
+ *
+ * @param durationMillis The length of the transition in milliseconds
+ */
+ public void startTransition(int durationMillis) {
+ mFrom = 0;
+ mTo = 255;
+ mAlpha = 0;
+ mFromScale = 0f;
+ mToScale = 1.0f;
+ mDuration = durationMillis;
+ mTransitionState = TRANSITION_STARTING;
+ invalidateSelf();
+ }
+
+ /**
+ * Show only the first layer.
+ */
+ public void resetTransition() {
+ mAlpha = 0;
+ mTransitionState = TRANSITION_NONE;
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ boolean done = true;
+ float scale = 0f;
+
+ switch (mTransitionState) {
+ case TRANSITION_STARTING:
+ mStartTimeMillis = SystemClock.uptimeMillis();
+ done = false;
+ mTransitionState = TRANSITION_RUNNING;
+ break;
+
+ case TRANSITION_RUNNING:
+ if (mStartTimeMillis >= 0) {
+ float normalized = (float)
+ (SystemClock.uptimeMillis() - mStartTimeMillis) / mDuration;
+ done = normalized >= 1.0f;
+ normalized = Math.min(normalized, 1.0f);
+ mAlpha = (int) (mFrom + (mTo - mFrom) * normalized);
+ scale = mFromScale + (mToScale - mFromScale) * normalized;
+ }
+ break;
+ }
+
+ final int alpha = mAlpha;
+
+ if (done) {
+ // the setAlpha() calls below trigger invalidation and redraw. If we're done, just draw
+ // the appropriate drawable[s] and return
+ if (alpha == 0) {
+ getDrawable(0).draw(canvas);
+ }
+ if (alpha == 0xFF) {
+ getDrawable(1).draw(canvas);
+
+ }
+ return;
+ }
+
+ Drawable d;
+ d = getDrawable(0);
+ d.setAlpha(255 - alpha);
+ int cx = getIntrinsicWidth() / 2;
+ int cy = getIntrinsicHeight() / 2;
+ canvas.save();
+ canvas.scale(1.0f - scale, 1.0f - scale, cx, cy);
+ d.draw(canvas);
+ canvas.restore();
+ d.setAlpha(0xFF);
+
+ if (alpha > 0) {
+ d = getDrawable(1);
+ d.setAlpha(alpha);
+ canvas.save();
+ canvas.scale(scale, scale, cx, cy);
+ d.draw(canvas);
+ canvas.restore();
+ d.setAlpha(0xFF);
+ }
+
+ if (!done) {
+ invalidateSelf();
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/MyThemeFragment.java b/src/org/cyanogenmod/theme/chooser/MyThemeFragment.java
new file mode 100644
index 0000000..90a3f05
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/MyThemeFragment.java
@@ -0,0 +1,707 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.WallpaperManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.MediaPlayer;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.content.Loader;
+import android.util.Log;
+import android.util.MutableLong;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.cyanogenmod.theme.util.AudioUtils;
+import org.cyanogenmod.theme.util.CursorLoaderHelper;
+import org.cyanogenmod.theme.util.PreferenceUtils;
+import org.cyanogenmod.theme.util.ThemedTypefaceHelper;
+import org.cyanogenmod.theme.util.TypefaceHelperCache;
+import org.cyanogenmod.theme.util.Utils;
+
+import cyanogenmod.providers.ThemesContract;
+import cyanogenmod.providers.ThemesContract.PreviewColumns;
+import cyanogenmod.providers.ThemesContract.ThemesColumns;
+import cyanogenmod.themes.ThemeChangeRequest;
+import cyanogenmod.themes.ThemeChangeRequest.RequestType;
+import cyanogenmod.themes.ThemeManager;
+
+import org.cyanogenmod.internal.util.ThemeUtils;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_ALL;
+
+public class MyThemeFragment extends ThemeFragment {
+ private static final String TAG = MyThemeFragment.class.getSimpleName();
+
+ private static final String ARG_BASE_THEME_PACKAGE_NAME = "baseThemePkgName";
+ private static final String ARG_BASE_THEME_NAME = "baseThemeName";
+ private static final String ARG_BASE_THEME_AUTHOR = "baseThemeAuthor";
+
+ private String mBaseThemeName;
+ private String mBaseThemeAuthor;
+
+ private SurfaceView mSurfaceView;
+
+ static MyThemeFragment newInstance(String baseThemePkgName, String baseThemeName,
+ String baseThemeAuthor, boolean skipLoadingAnim,
+ boolean animateToLockScreenCard) {
+ MyThemeFragment f = new MyThemeFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_PACKAGE_NAME, CURRENTLY_APPLIED_THEME);
+ args.putString(ARG_BASE_THEME_PACKAGE_NAME, baseThemePkgName);
+ args.putString(ARG_BASE_THEME_NAME, baseThemeName);
+ args.putString(ARG_BASE_THEME_AUTHOR, baseThemeAuthor);
+ args.putBoolean(ARG_SKIP_LOADING_ANIM, skipLoadingAnim);
+ args.putBoolean(ARG_ANIMATE_TO_LOCK_SCREEN_CARD, animateToLockScreenCard);
+ f.setArguments(args);
+ return f;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final Context context = getActivity();
+ ThemedTypefaceHelper helper = sTypefaceHelperCache.getHelperForTheme(context,
+ getAppliedFontPackageName());
+ mTypefaceNormal = helper.getTypeface(Typeface.NORMAL);
+ mBaseThemePkgName = getArguments().getString(ARG_BASE_THEME_PACKAGE_NAME);
+ mBaseThemeName = getArguments().getString(ARG_BASE_THEME_NAME);
+ mBaseThemeAuthor = getArguments().getString(ARG_BASE_THEME_AUTHOR);
+ mShowLockScreenSelectorAfterContentLoaded = getArguments().getBoolean(
+ ARG_ANIMATE_TO_LOCK_SCREEN_CARD);
+ mSurfaceView = createSurfaceView();
+ populateBaseThemeSupportedComponents(mBaseThemePkgName);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = super.onCreateView(inflater, container, savedInstanceState);
+ mThemeTagLayout.setAppliedTagEnabled(true);
+ if (mBaseThemePkgName.equals(Utils.getDefaultThemePackageName(getActivity()))) {
+ mThemeTagLayout.setDefaultTagEnabled(true);
+ }
+ if (PreferenceUtils.hasThemeBeenUpdated(getActivity(), mBaseThemePkgName)) {
+ mThemeTagLayout.setUpdatedTagEnabled(true);
+ }
+ mDelete.setVisibility(View.GONE);
+ setCustomized(isThemeCustomized());
+ return v;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (!mExpanded && getLoaderManager().getLoader(0) != null) {
+ getLoaderManager().restartLoader(0, null, this);
+ }
+
+ IntentFilter filter = new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED);
+ getActivity().registerReceiver(mWallpaperChangeReceiver, filter);
+ }
+
+ @Override
+ public void onPause() {
+ getActivity().unregisterReceiver(mWallpaperChangeReceiver);
+ super.onPause();
+ }
+
+ @Override
+ public void setUserVisibleHint(boolean isVisibleToUser) {
+ super.setUserVisibleHint(isVisibleToUser);
+ if (mThemeTagLayout == null) return;
+
+ if (!isVisibleToUser) {
+ if (PreferenceUtils.hasThemeBeenUpdated(getActivity(), mBaseThemePkgName)) {
+ mThemeTagLayout.setUpdatedTagEnabled(true);
+ }
+ } else {
+ if (PreferenceUtils.hasThemeBeenUpdated(getActivity(), mBaseThemePkgName)) {
+ PreferenceUtils.removeUpdatedTheme(getActivity(), mBaseThemePkgName);
+ }
+ }
+ }
+
+ @Override
+ protected boolean onPopupMenuItemClick(MenuItem item) {
+ switch(item.getItemId()) {
+ case R.id.menu_reset:
+ resetTheme();
+ return true;
+ }
+
+ return super.onPopupMenuItemClick(item);
+ }
+
+ @Override
+ public void collapse(boolean applyTheme) {
+ super.collapse(applyTheme);
+ if (mSurfaceView != null) mSurfaceView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void expand() {
+ super.expand();
+ if (mSurfaceView != null && mShadowFrame.indexOfChild(mSurfaceView) >= 0) {
+ mSurfaceView.setVisibility(View.GONE);
+ mWallpaper.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void performClick(boolean clickedOnContent) {
+ if (clickedOnContent) {
+ showCustomizeResetLayout();
+ } else {
+ if (isShowingCustomizeResetLayout()) {
+ hideCustomizeResetLayout();
+ } else {
+ super.performClick(clickedOnContent);
+ }
+ }
+ }
+
+ @Override
+ public void setCurrentTheme(Map<String, String> currentTheme,
+ MutableLong currentWallpaperComponentId) {
+ super.setCurrentTheme(currentTheme, currentWallpaperComponentId);
+ for (String key : currentTheme.keySet()) {
+ mSelectedComponentsMap.put(key, currentTheme.get(key));
+ }
+ mSelectedWallpaperComponentId = currentWallpaperComponentId.value;
+ }
+
+ @Override
+ public boolean componentsChanged() {
+ // If an external wallpaper/ls are set then something changed!
+ if (mExternalWallpaperUri != null || mExternalLockscreenUri != null) return true;
+
+ for (String key : mSelectedComponentsMap.keySet()) {
+ String current = mCurrentTheme.get(key);
+ if (current == null || !current.equals(mSelectedComponentsMap.get(key))) {
+ return true;
+ }
+ if (ThemesColumns.MODIFIES_LAUNCHER.equals(key) &&
+ mCurrentWallpaperComponentId.value != mSelectedWallpaperComponentId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected void applyThemeWhenPopulated(String pkgName, List<String> components) {
+ super.applyThemeWhenPopulated(pkgName, components);
+ populateComponentsToApply(pkgName, components);
+ }
+
+ private void populateComponentsToApply(String pkgName, List<String> components) {
+ String selection = ThemesColumns.PKG_NAME + "=?";
+ String[] selectionArgs = { pkgName };
+ Cursor c = getActivity().getContentResolver().query(ThemesColumns.CONTENT_URI,
+ null, selection, selectionArgs, null);
+ if (c != null) {
+ if (c.getCount() > 0 && c.moveToFirst()) {
+ mSelectedComponentsMap.clear();
+ if (c.getInt(c.getColumnIndex(ThemesColumns.MODIFIES_ALARMS)) == 1) {
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_ALARMS, pkgName);
+ }
+ if (c.getInt(c.getColumnIndex(ThemesColumns.MODIFIES_BOOT_ANIM)) == 1) {
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_BOOT_ANIM, pkgName);
+ }
+ if (c.getInt(c.getColumnIndex(ThemesColumns.MODIFIES_FONTS)) == 1) {
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_FONTS, pkgName);
+ }
+ if (c.getInt(c.getColumnIndex(ThemesColumns.MODIFIES_ICONS)) == 1) {
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_ICONS, pkgName);
+ }
+ if (c.getInt(c.getColumnIndex(ThemesColumns.MODIFIES_LAUNCHER)) == 1) {
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_LAUNCHER, pkgName);
+ }
+ if (c.getInt(c.getColumnIndex(ThemesColumns.MODIFIES_LOCKSCREEN)) == 1) {
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_LOCKSCREEN, pkgName);
+ }
+ if (c.getInt(c.getColumnIndex(ThemesColumns.MODIFIES_NAVIGATION_BAR)) == 1) {
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_NAVIGATION_BAR, pkgName);
+ }
+ if (c.getInt(c.getColumnIndex(ThemesColumns.MODIFIES_NOTIFICATIONS)) == 1) {
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_NOTIFICATIONS, pkgName);
+ }
+ if (c.getInt(c.getColumnIndex(ThemesColumns.MODIFIES_OVERLAYS)) == 1) {
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_OVERLAYS, pkgName);
+ }
+ if (c.getInt(c.getColumnIndex(ThemesColumns.MODIFIES_RINGTONES)) == 1) {
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_RINGTONES, pkgName);
+ }
+ if (c.getInt(c.getColumnIndex(ThemesColumns.MODIFIES_STATUS_BAR)) == 1) {
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_STATUS_BAR, pkgName);
+ }
+ if (c.getInt(c.getColumnIndex(ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN)) == 1) {
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN, pkgName);
+ }
+ }
+ c.close();
+ }
+
+ // strip out any components that are not in the components list
+ if (components != null) {
+ Iterator<Map.Entry<String, String>> iterator =
+ mSelectedComponentsMap.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry<String, String> entry = iterator.next();
+ if (!components.contains(entry.getKey())) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ private void loadComponentsToApply() {
+ for (String component : mSelectedComponentsMap.keySet()) {
+ loadComponentFromPackage(mSelectedComponentsMap.get(component), component,
+ mSelectedWallpaperComponentId);
+ }
+ }
+
+ private BroadcastReceiver mWallpaperChangeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // only update if we are the current visible fragment or if there is no theme
+ // being applied.
+ ThemeManager tm = getThemeManager();
+ if (!tm.isThemeApplying() || getUserVisibleHint()) {
+ final WallpaperManager wm = WallpaperManager.getInstance(context);
+ if (wm.getWallpaperInfo() != null) {
+ addSurfaceView(mSurfaceView);
+ } else {
+ removeSurfaceView(mSurfaceView);
+ }
+
+ Drawable wp = context == null ? null : wm.getDrawable();
+ if (wp != null) {
+ mWallpaper.setImageDrawable(wp);
+ mWallpaperCard.setWallpaper(wp);
+ }
+ }
+ }
+ };
+
+ private void setCustomized(boolean customized) {
+ mReset.setVisibility(customized ? View.VISIBLE : View.GONE);
+ mThemeTagLayout.setCustomizedTagEnabled(customized);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_ID_ALL:
+ if (args != null) {
+ String pkgName = args.getString(ARG_PACKAGE_NAME);
+ if (pkgName != null) {
+ return super.onCreateLoader(id, args);
+ }
+ }
+ return CursorLoaderHelper.myThemeFragmentCursorLoader(getActivity(), id);
+ default:
+ // Only LOADER_ID_ALL differs for MyThemeFragment
+ return super.onCreateLoader(id, args);
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
+ super.onLoadFinished(loader, c);
+ // if the theme is resetting, we need to apply these changes now that the supported
+ // theme components have been properly set.
+ if (loader.getId() == LOADER_ID_ALL) {
+ if (mThemeResetting) {
+ applyTheme();
+ } else if (mApplyThemeOnPopulated) {
+ loadComponentsToApply();
+ applyTheme();
+ } else if (mSelectedComponentsMap.size() == 0) {
+ //Re-populates selected components with current theme. Why?
+ //We got here because the cursor was reloaded after the user pressed back and no
+ //changes were applied, causing the selected components map to be wiped out
+ mSelectedComponentsMap.putAll(mCurrentTheme);
+ }
+ }
+ }
+
+ @Override
+ protected Map<String, String> fillMissingComponentsWithDefault(
+ Map<String, String> originalMap) {
+ // Only the ThemeFragment should be altering this, for the MyThemeFragment this is not
+ // desirable as it changes components the user did not even touch.
+ return originalMap;
+ }
+
+ @Override
+ protected ThemeChangeRequest getThemeChangeRequestForComponents(
+ Map<String, String> componentMap) {
+ return getThemeChangeRequestForComponents(componentMap, RequestType.USER_REQUEST_MIXNMATCH);
+ }
+
+ @Override
+ protected Map<String, String> getComponentsToApply() {
+ Map<String, String> componentsToApply = mThemeResetting
+ ? getEmptyComponentsMap()
+ : new HashMap<String, String>();
+ if (mThemeResetting) {
+ final String pkgName = getThemePackageName();
+ for (String component : mBaseThemeSupportedComponents) {
+ componentsToApply.put(component, pkgName);
+ }
+ } else {
+ // Only apply components that actually changed
+ for (String component : mSelectedComponentsMap.keySet()) {
+ String currentPkg = mCurrentTheme.get(component);
+ String selectedPkg = mSelectedComponentsMap.get(component);
+ if (currentPkg == null || mThemeResetting || !currentPkg.equals(selectedPkg) ||
+ mCurrentWallpaperComponentId.value != mSelectedWallpaperComponentId) {
+ componentsToApply.put(component, selectedPkg);
+ }
+ }
+ if (mExternalLockscreenUri != null) {
+ if (mCurrentTheme.containsKey(ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN)) {
+ componentsToApply.put(ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN, LOCKSCREEN_NONE);
+ }
+ if (mCurrentTheme.containsKey(ThemesColumns.MODIFIES_LOCKSCREEN)) {
+ componentsToApply.put(ThemesColumns.MODIFIES_LOCKSCREEN, LOCKSCREEN_NONE);
+ }
+ }
+ }
+ return componentsToApply;
+ }
+
+ @Override
+ protected void populateSupportedComponents(Cursor c) {
+ }
+
+ @Override
+ protected Boolean shouldShowComponentCard(String component) {
+ return true;
+ }
+
+ @Override
+ protected void loadTitle(Cursor c) {
+ mTitle.setText(mBaseThemeName);
+ mAuthor.setText(mBaseThemeAuthor);
+ }
+
+ @Override
+ protected void loadWallpaper(Cursor c, boolean animate) {
+ mExternalWallpaperUri = null;
+ int pkgNameIdx = c.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME);
+ if (pkgNameIdx > -1) {
+ super.loadWallpaper(c, animate);
+ return;
+ }
+ Drawable overlay = null;
+ if (animate) {
+ overlay = getOverlayDrawable(mWallpaperCard, true);
+ }
+
+ int wpIdx = c.getColumnIndex(PreviewColumns.WALLPAPER_PREVIEW);
+ final Resources res = getResources();
+ final Context context = getActivity();
+ final WallpaperManager wm = WallpaperManager.getInstance(context);
+ if (wm.getWallpaperInfo() != null) {
+ addSurfaceView(mSurfaceView);
+ } else {
+ removeSurfaceView(mSurfaceView);
+ }
+
+ Drawable wp = context == null ? null : wm.getDrawable();
+ if (wp == null) {
+ Bitmap bmp = Utils.loadBitmapBlob(c, wpIdx);
+ if (bmp != null) wp = new BitmapDrawable(res, bmp);
+ }
+ if (wp != null) {
+ mWallpaper.setImageDrawable(wp);
+ mWallpaperCard.setWallpaper(wp);
+ setCardTitle(mWallpaperCard, mCurrentTheme.get(ThemesColumns.MODIFIES_LAUNCHER),
+ getString(R.string.wallpaper_label));
+ } else {
+ mWallpaperCard.clearWallpaper();
+ mWallpaperCard.setEmptyViewEnabled(true);
+ setAddComponentTitle(mWallpaperCard, getString(R.string.wallpaper_label));
+ }
+
+ if (animate) {
+ animateContentChange(R.id.wallpaper_card, mWallpaperCard, overlay);
+ }
+ }
+
+ @Override
+ protected void loadLockScreen(Cursor c, boolean animate) {
+ mExternalLockscreenUri = null;
+ int pkgNameIdx = c.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME);
+ if (pkgNameIdx > -1) {
+ super.loadLockScreen(c, animate);
+ return;
+ }
+ Drawable overlay = null;
+ if (animate) {
+ overlay = getOverlayDrawable(mLockScreenCard, true);
+ }
+
+ //If the current theme includes a lock wallpaper, the WallpaperMgr will
+ //return a valid Drawable we can display in the card. However, if the user
+ //picked a LLS, we need to get the path from the provider and manually load the bitmap
+ int wpIdx = c.getColumnIndex(PreviewColumns.LIVE_LOCK_SCREEN_PREVIEW);
+ Drawable wp = null;
+ if (wpIdx >= 0) {
+ final Resources res = getResources();
+ Bitmap bmp = Utils.loadBitmapBlob(c, wpIdx);
+ if (bmp != null) wp = new BitmapDrawable(res, bmp);
+ } else {
+ final Context context = getActivity();
+ wp = context == null ? null :
+ WallpaperManager.getInstance(context).getFastKeyguardDrawable();
+ }
+ if (wp != null) {
+ mLockScreenCard.setWallpaper(wp);
+ } else if (!mSelectedComponentsMap.containsKey(ThemesColumns.MODIFIES_LOCKSCREEN)) {
+ mLockScreenCard.clearWallpaper();
+ mLockScreenCard.setEmptyViewEnabled(true);
+ setAddComponentTitle(mLockScreenCard, getString(R.string.lockscreen_label));
+ }
+
+ if (animate) {
+ animateContentChange(R.id.lockscreen_card, mLockScreenCard, overlay);
+ }
+ }
+
+ @Override
+ protected void loadFont(Cursor c, boolean animate) {
+ int pkgNameIdx = c.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME);
+ if (pkgNameIdx > -1) {
+ super.loadFont(c, animate);
+ return;
+ }
+ Drawable overlay = null;
+ if (animate) {
+ overlay = getOverlayDrawable(mFontPreview, true);
+ }
+ setCardTitle(mFontCard, mCurrentTheme.get(ThemesColumns.MODIFIES_FONTS),
+ getString(R.string.font_label));
+
+ TypefaceHelperCache cache = TypefaceHelperCache.getInstance();
+ ThemedTypefaceHelper helper = cache.getHelperForTheme(getActivity(),
+ getAppliedFontPackageName());
+ mTypefaceNormal = helper.getTypeface(Typeface.NORMAL);
+ mFontPreview.setTypeface(mTypefaceNormal);
+ if (animate) {
+ animateContentChange(R.id.font_preview_container, mFontPreview, overlay);
+ }
+ }
+
+ @Override
+ protected void loadAudible(int type, Cursor c, boolean animate) {
+ int pkgNameIdx = c.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME);
+ if (pkgNameIdx > -1) {
+ super.loadAudible(type, c, animate);
+ return;
+ }
+ ComponentCardView audibleContainer = null;
+ ImageView playPause = null;
+ String modsComponent = "";
+ switch (type) {
+ case RingtoneManager.TYPE_RINGTONE:
+ audibleContainer = mRingtoneCard;
+ playPause = mRingtonePlayPause;
+ modsComponent = ThemesColumns.MODIFIES_RINGTONES;
+ break;
+ case RingtoneManager.TYPE_NOTIFICATION:
+ audibleContainer = mNotificationCard;
+ playPause = mNotificationPlayPause;
+ modsComponent = ThemesColumns.MODIFIES_NOTIFICATIONS;
+ break;
+ case RingtoneManager.TYPE_ALARM:
+ audibleContainer = mAlarmCard;
+ playPause = mAlarmPlayPause;
+ modsComponent = ThemesColumns.MODIFIES_ALARMS;
+ break;
+ }
+ if (audibleContainer == null) return;
+
+ if (playPause == null) {
+ playPause =
+ (ImageView) audibleContainer.findViewById(R.id.play_pause);
+ }
+ TextView title = (TextView) audibleContainer.findViewById(R.id.audible_name);
+ MediaPlayer mp = mMediaPlayers.get(playPause);
+ if (mp == null) {
+ mp = new MediaPlayer();
+ }
+
+ final Context context = getActivity();
+ Ringtone ringtone = null;
+ try {
+ Uri ringtoneUri = AudioUtils.loadDefaultAudible(context, type, mp);
+ if (ringtoneUri != null) ringtone = RingtoneManager.getRingtone(context, ringtoneUri);
+ } catch (IOException e) {
+ Log.w(TAG, "Unable to load default sound ", e);
+ }
+
+ if (ringtone != null) {
+ title.setText(ringtone.getTitle(context));
+ setCardTitle(audibleContainer, mCurrentTheme.get(modsComponent),
+ getAudibleLabel(type));
+ } else {
+ title.setText(getString(R.string.audible_title_none));
+ setAddComponentTitle(audibleContainer, getAudibleLabel(type));
+ playPause.setVisibility(View.INVISIBLE);
+ audibleContainer.setEmptyViewEnabled(true);
+ }
+
+ playPause.setTag(mp);
+ mMediaPlayers.put(playPause, mp);
+ playPause.setOnClickListener(mPlayPauseClickListener);
+ mp.setOnCompletionListener(mPlayCompletionListener);
+ }
+
+ @Override
+ protected void loadStatusBar(Cursor c, boolean animate) {
+ super.loadStatusBar(c, animate);
+ setCardTitle(mStatusBarCard, mCurrentTheme.get(ThemesColumns.MODIFIES_STATUS_BAR),
+ getString(R.string.statusbar_label));
+ }
+
+ @Override
+ protected void loadIcons(Cursor c, boolean animate) {
+ super.loadIcons(c, animate);
+ setCardTitle(mIconCard, mCurrentTheme.get(ThemesColumns.MODIFIES_ICONS),
+ getString(R.string.icon_label));
+ }
+
+ @Override
+ protected void loadNavBar(Cursor c, boolean animate) {
+ super.loadNavBar(c, animate);
+ setCardTitle(mNavBarCard, mCurrentTheme.get(ThemesColumns.MODIFIES_NAVIGATION_BAR),
+ getString(R.string.navbar_label));
+ }
+
+ @Override
+ protected void loadStyle(Cursor c, boolean animate) {
+ super.loadStyle(c, animate);
+ setCardTitle(mStyleCard, mCurrentTheme.get(ThemesColumns.MODIFIES_OVERLAYS),
+ getString(R.string.style_label));
+ }
+
+ @Override
+ protected void loadBootAnimation(Cursor c) {
+ super.loadBootAnimation(c);
+ setCardTitle(mBootAnimationCard, mCurrentTheme.get(ThemesColumns.MODIFIES_BOOT_ANIM),
+ getString(R.string.boot_animation_label));
+ }
+
+ @Override
+ public String getThemePackageName() {
+ if (mBaseThemePkgName == null) {
+ // check if the package name is defined in the arguments bundle
+ Bundle bundle = getArguments();
+ if (bundle != null) {
+ mBaseThemePkgName = bundle.getString(ARG_BASE_THEME_PACKAGE_NAME);
+ }
+ }
+ return mBaseThemePkgName;
+ }
+
+ private SurfaceView createSurfaceView() {
+ final Context context = getActivity();
+ if (context == null) return null;
+
+ SurfaceView sv = new SurfaceView(context);
+ final Resources res = context.getResources();
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+ res.getDimensionPixelSize(R.dimen.wallpaper_preview_width),
+ res.getDimensionPixelSize(R.dimen.theme_preview_height),
+ Gravity.CENTER_HORIZONTAL);
+ sv.setLayoutParams(params);
+
+ return sv;
+ }
+
+ private void addSurfaceView(SurfaceView sv) {
+ if (mShadowFrame.indexOfChild(mSurfaceView) < 0) {
+ int idx = mShadowFrame.indexOfChild(mWallpaper);
+ mShadowFrame.addView(sv, idx + 1);
+ }
+ }
+
+ private void removeSurfaceView(SurfaceView sv) {
+ if (mShadowFrame.indexOfChild(mSurfaceView) >= 0) {
+ mShadowFrame.removeView(sv);
+ }
+ }
+
+ /**
+ * Populates mBaseThemeSupportedComponents.
+ * @param pkgName Package name of the base theme used
+ */
+ private void populateBaseThemeSupportedComponents(String pkgName) {
+ String selection = ThemesColumns.PKG_NAME + "=?";
+ String[] selectionArgs = { pkgName };
+ Cursor c = getActivity().getContentResolver().query(ThemesColumns.CONTENT_URI,
+ null, selection, selectionArgs, null);
+ if (c != null) {
+ if (c.moveToFirst()) {
+ List<String> components = ThemeUtils.getAllComponents();
+ final String baseThemePackageName = getThemePackageName();
+ for (String component : components) {
+ int pkgIdx = c.getColumnIndex(ThemesColumns.PKG_NAME);
+ int modifiesCompIdx = c.getColumnIndex(component);
+
+ String pkg = pkgIdx >= 0 ? c.getString(pkgIdx) : null;
+ boolean supported = (modifiesCompIdx >= 0) && (c.getInt(modifiesCompIdx) == 1);
+ if (supported && baseThemePackageName.equals(pkg)) {
+ mBaseThemeSupportedComponents.add(component);
+ }
+ }
+ }
+ c.close();
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/NewFragmentStatePagerAdapter.java b/src/org/cyanogenmod/theme/chooser/NewFragmentStatePagerAdapter.java
new file mode 100644
index 0000000..cefc344
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/NewFragmentStatePagerAdapter.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.PagerAdapter;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Implementation of {@link android.support.v4.view.PagerAdapter} that
+ * uses a {@link android.support.v4.app.Fragment} to manage each page. This class also handles
+ * saving and restoring of fragment's state.
+ *
+ * <p>This version of the pager is more useful when there are a large number
+ * of pages, working more like a list view. When pages are not visible to
+ * the user, their entire fragment may be destroyed, only keeping the saved
+ * state of that fragment. This allows the pager to hold on to much less
+ * memory associated with each visited page as compared to
+ * {@link android.support.v4.app.FragmentPagerAdapter} at the cost of potentially more overhead when
+ * switching between pages.
+ *
+ * <p>When using FragmentPagerAdapter the host ViewPager must have a
+ * valid ID set.</p>
+ *
+ * <p>Subclasses only need to implement {@link #getItem(int)}
+ * and {@link #getCount()} to have a working adapter.
+ *
+ * <p>Here is an example implementation of a pager containing fragments of
+ * lists:
+ *
+ * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentStatePagerSupport.java
+ * complete}
+ *
+ * <p>The <code>R.layout.fragment_pager</code> resource of the top-level fragment is:
+ *
+ * {@sample development/samples/Support13Demos/res/layout/fragment_pager.xml
+ * complete}
+ *
+ * <p>The <code>R.layout.fragment_pager_list</code> resource containing each
+ * individual fragment's layout is:
+ *
+ * {@sample development/samples/Support13Demos/res/layout/fragment_pager_list.xml
+ * complete}
+ */
+public abstract class NewFragmentStatePagerAdapter extends PagerAdapter {
+ private static final String TAG = NewFragmentStatePagerAdapter.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private final FragmentManager mFragmentManager;
+ private FragmentTransaction mCurTransaction = null;
+
+ private long[] mItemIds = new long[] {};
+ private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
+ private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
+ private Fragment mCurrentPrimaryItem = null;
+
+ public NewFragmentStatePagerAdapter(FragmentManager fm) {
+
+ mFragmentManager = fm;
+ createIdCache();
+ }
+
+ /**
+ * Return the Fragment associated with a specified position.
+ */
+ public abstract Fragment getItem(int position);
+
+ /**
+ * Return a unique identifier for the item at the given position.
+ */
+ public abstract long getItemId(int position);
+
+ @Override
+ public void startUpdate(ViewGroup container) {
+ }
+
+ private void checkForIdChanges() {
+ long[] newItemIds = new long[getCount()];
+ for (int i = 0; i < newItemIds.length; i++) {
+ newItemIds[i] = getItemId(i);
+ }
+
+ if (!Arrays.equals(mItemIds, newItemIds)) {
+ ArrayList<Fragment.SavedState> newSavedState = new ArrayList<Fragment.SavedState>();
+ ArrayList<Fragment> newFragments = new ArrayList<Fragment>();
+
+ for (int oldPosition = 0; oldPosition < mItemIds.length; oldPosition++) {
+ int newPosition = POSITION_NONE;
+ for (int i = 0; i < newItemIds.length; i++) {
+ if (mItemIds[oldPosition] == newItemIds[i]) {
+ newPosition = i;
+ break;
+ }
+ }
+ if (newPosition >= 0) {
+ if (oldPosition < mSavedState.size()) {
+ Fragment.SavedState savedState = mSavedState.get(oldPosition);
+ if (savedState != null) {
+ while (newSavedState.size() <= newPosition) {
+ newSavedState.add(null);
+ }
+ newSavedState.set(newPosition, savedState);
+ }
+ }
+ if (oldPosition < mFragments.size()) {
+ Fragment fragment = mFragments.get(oldPosition);
+ if (fragment != null) {
+ while (newFragments.size() <= newPosition) {
+ newFragments.add(null);
+ }
+ newFragments.set(newPosition, fragment);
+ }
+ }
+ }
+ }
+
+ mItemIds = newItemIds;
+ mSavedState = newSavedState;
+ mFragments = newFragments;
+ }
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ checkForIdChanges();
+
+ super.notifyDataSetChanged();
+ }
+
+ /**
+ * Create the initial set of item IDs. Run this after you have set your adapter data.
+ */
+ public void createIdCache() {
+ // If we have already stored ids, don't overwrite them
+ if (mItemIds.length == 0) {
+ // getCount might have overhead, so run it as late as possible
+ final int count = getCount();
+ if (count > 0) {
+ mItemIds = new long[count];
+ for (int i = 0; i < count; i++) {
+ mItemIds[i] = getItemId(i);
+ }
+ }
+ }
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+
+ createIdCache();
+
+ // If we already have this item instantiated, there is nothing
+ // to do. This can happen when we are restoring the entire pager
+ // from its saved state, where the fragment manager has already
+ // taken care of restoring the fragments we previously had instantiated.
+ if (mFragments.size() > position) {
+ Fragment f = mFragments.get(position);
+ if (f != null) {
+ return f;
+ }
+ }
+
+ if (mCurTransaction == null) {
+ mCurTransaction = mFragmentManager.beginTransaction();
+ }
+
+ Fragment fragment = getItem(position);
+ if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
+ if (mSavedState.size() > position) {
+ Fragment.SavedState fss = mSavedState.get(position);
+ if (fss != null) {
+ fragment.setInitialSavedState(fss);
+ }
+ }
+ while (mFragments.size() <= position) {
+ mFragments.add(null);
+ }
+ fragment.setMenuVisibility(false);
+ fragment.setUserVisibleHint(false);
+ mFragments.set(position, fragment);
+ mCurTransaction.add(container.getId(), fragment);
+
+ return fragment;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ Fragment fragment = (Fragment)object;
+
+ if (mCurTransaction == null) {
+ mCurTransaction = mFragmentManager.beginTransaction();
+ }
+ if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
+ + " v=" + ((Fragment)object).getView());
+ while (mSavedState.size() <= position) {
+ mSavedState.add(null);
+ }
+ mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
+
+ // Only set the position to null if the fragment being removed is at "position"
+ // We do this because checkForIdChanges updates the mFragments list and if a fragment
+ // was removed then the fragment at "position" is not the fragment that was removed.
+ if (position < mFragments.size() && fragment == mFragments.get(position)) {
+ mFragments.set(position, null);
+ }
+ mCurTransaction.remove(fragment);
+ }
+
+ @Override
+ public void setPrimaryItem(ViewGroup container, int position, Object object) {
+ Fragment fragment = (Fragment)object;
+ if (fragment != mCurrentPrimaryItem) {
+ if (mCurrentPrimaryItem != null) {
+ mCurrentPrimaryItem.setMenuVisibility(false);
+ mCurrentPrimaryItem.setUserVisibleHint(false);
+ }
+ if (fragment != null) {
+ fragment.setMenuVisibility(true);
+ fragment.setUserVisibleHint(true);
+ }
+ mCurrentPrimaryItem = fragment;
+ }
+ }
+
+ @Override
+ public void finishUpdate(ViewGroup container) {
+ if (mCurTransaction != null) {
+ mCurTransaction.commitAllowingStateLoss();
+ mCurTransaction = null;
+ mFragmentManager.executePendingTransactions();
+ }
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return ((Fragment)object).getView() == view;
+ }
+
+ @Override
+ public Parcelable saveState() {
+ Bundle state = null;
+
+ mItemIds = new long[getCount()];
+ for (int i = 0; i < mItemIds.length; i++) {
+ mItemIds[i] = getItemId(i);
+ }
+ if (mSavedState.size() > 0) {
+ state = new Bundle();
+
+ if (mItemIds.length > 0) {
+ state.putLongArray("itemids", mItemIds);
+ }
+
+ Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
+ mSavedState.toArray(fss);
+ state.putParcelableArray("states", fss);
+ }
+ for (int i=0; i<mFragments.size(); i++) {
+ Fragment f = mFragments.get(i);
+ if (f != null) {
+ if (state == null) {
+ state = new Bundle();
+ }
+ String key = "f" + i;
+ mFragmentManager.putFragment(state, key, f);
+ }
+ }
+ return state;
+ }
+
+ @Override
+ public void restoreState(Parcelable state, ClassLoader loader) {
+ if (state != null) {
+ Bundle bundle = (Bundle)state;
+ bundle.setClassLoader(loader);
+
+ mItemIds = bundle.getLongArray("itemids");
+ if (mItemIds == null) {
+ mItemIds = new long[] {};
+ }
+
+ Parcelable[] fss = bundle.getParcelableArray("states");
+ mSavedState.clear();
+ mFragments.clear();
+ if (fss != null) {
+ for (int i=0; i<fss.length; i++) {
+ mSavedState.add((Fragment.SavedState)fss[i]);
+ }
+ }
+ Iterable<String> keys = bundle.keySet();
+ for (String key: keys) {
+ if (key.startsWith("f")) {
+ int index = Integer.parseInt(key.substring(1));
+ Fragment f = mFragmentManager.getFragment(bundle, key);
+ if (f != null) {
+ while (mFragments.size() <= index) {
+ mFragments.add(null);
+ }
+ f.setMenuVisibility(false);
+ mFragments.set(index, f);
+ } else {
+ Log.w(TAG, "Bad fragment at key " + key);
+ }
+ }
+ }
+ checkForIdChanges();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/org/cyanogenmod/theme/chooser/NotificationHijackingService.java b/src/org/cyanogenmod/theme/chooser/NotificationHijackingService.java
new file mode 100644
index 0000000..dddd8f5
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/NotificationHijackingService.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.themeInfo != null) {
+ cancelNotification(sbn.getKey());
+ }
+ }
+ } 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);
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/PagerContainer.java b/src/org/cyanogenmod/theme/chooser/PagerContainer.java
new file mode 100644
index 0000000..852b847
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/PagerContainer.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.graphics.Point;
+import android.support.v4.view.ThemeViewPager;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+/**
+ * PagerContainer: A layout that displays a ViewPager with its children that are outside
+ * the typical pager bounds.
+ */
+public class PagerContainer extends FrameLayout implements ViewPager.OnPageChangeListener {
+ private static final int ANIMATE_OUT_DURATION = 300;
+ private static final int ANIMATE_OUT_INTERPOLATE_FACTOR = 1;
+ private static final int ANIMATE_IN_DURATION = 300;
+ private static final int ANIMATE_IN_INTERPOLATE_FACTOR = 2;
+
+ private ThemeViewPager mPager;
+ private Point mCenter = new Point();
+ private Point mInitialTouch = new Point();
+ private int mCollapsedHeight;
+ private boolean mIsAnimating = false;
+
+ boolean mNeedsRedraw = false;
+
+ public PagerContainer(Context context) {
+ this(context, null, 0);
+ }
+
+ public PagerContainer(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PagerContainer(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mCollapsedHeight = generateLayoutParams(attrs).height;
+
+ //Disable clipping of children so non-selected pages are visible
+ setClipChildren(false);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ try {
+ mPager = (ThemeViewPager) getChildAt(0);
+ mPager.setOnPageChangeListener(this);
+ } catch (Exception e) {
+ throw new IllegalStateException("The root child of PagerContainer must be a ViewPager");
+ }
+ }
+
+ public ThemeViewPager getViewPager() {
+ return mPager;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mCenter.x = w / 2;
+ mCenter.y = h / 2;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (mIsAnimating) return true;
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ // Do not allow touch events to propagate if we are animating
+ if (mIsAnimating) return true;
+
+ //We capture any touches not already handled by the ViewPager
+ // to implement scrolling from a touch outside the pager bounds.
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mInitialTouch.x = (int)ev.getX();
+ mInitialTouch.y = (int)ev.getY();
+ default:
+ ev.offsetLocation(mCenter.x - mInitialTouch.x, mCenter.y - mInitialTouch.y);
+ break;
+ }
+
+ return mPager.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ //Force the container to redraw on scrolling.
+ //Without this the outer pages render initially and then stay static
+ if (mNeedsRedraw) invalidate();
+ }
+
+ @Override
+ public void onPageSelected(int position) { }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ mNeedsRedraw = (state != ThemeViewPager.SCROLL_STATE_IDLE);
+ }
+
+ public void setIsAnimating(boolean isAnimating) {
+ mIsAnimating = isAnimating;
+ }
+
+ public void expand() {
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(getLayoutParams());
+ params.height = LinearLayout.LayoutParams.MATCH_PARENT;
+ setLayoutParams(params);
+
+ mPager.setExpanded(true);
+
+ final int current = mPager.getCurrentItem();
+ final int prevY = (int) getY();
+
+ //Since our viewpager's width is changing to fill the screen
+ //we must start the left/right children of the current page inwards on first draw
+ final int lChildPrevXf;
+ final int rChildPrevXf;
+
+ if (current != 0) {
+ final View lchild = mPager.getViewForPosition(current - 1);
+ lChildPrevXf = (int) lchild.getX();
+ } else {
+ lChildPrevXf = 0;
+ }
+
+ if (current < mPager.getAdapter().getCount() - 1) {
+ View rchild = mPager.getViewForPosition(current + 1);
+ rChildPrevXf = (int) rchild.getX();
+ } else {
+ rChildPrevXf = 0;
+ }
+
+
+ final ViewTreeObserver observer = mPager.getViewTreeObserver();
+ observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ public boolean onPreDraw() {
+ observer.removeOnPreDrawListener(this);
+ if (current != 0) {
+ View lchild = mPager.getViewForPosition(current - 1);
+ lchild.setTranslationY(prevY - getY());
+ lchild.setX(lChildPrevXf);
+ animateChildOut(lchild, -getWidth());
+ }
+
+ if (current < mPager.getAdapter().getCount() - 1) {
+ View rchild = mPager.getViewForPosition(current + 1);
+ rchild.setX(rChildPrevXf);
+ rchild.setTranslationY(prevY - getY());
+ animateChildOut(rchild, getWidth());
+ }
+ return false;
+ }
+ });
+ }
+
+ public void collapse() {
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(getLayoutParams());
+ params.height = mCollapsedHeight;
+ setLayoutParams(params);
+
+ mPager.setExpanded(false);
+ int current = mPager.getCurrentItem();
+ final int prevY = (int) getY();
+
+ if (current != 0) {
+ View lchild = mPager.getViewForPosition(current - 1);
+ lchild.setTranslationY(0);
+ animateChildIn(lchild);
+ }
+
+ if (current < mPager.getAdapter().getCount() - 1) {
+ View rchild = mPager.getViewForPosition(current + 1);
+ rchild.setTranslationY(0);
+ animateChildIn(rchild);
+ }
+ }
+
+ private void animateChildOut(final View v, float endX) {
+ v.animate()
+ .translationX(endX)
+ .setDuration(ANIMATE_OUT_DURATION)
+ .setInterpolator(new AccelerateInterpolator(ANIMATE_OUT_INTERPOLATE_FACTOR));
+ }
+
+ private void animateChildIn(final View v) {
+ v.animate()
+ .translationX(0)
+ .setDuration(ANIMATE_IN_DURATION)
+ .setInterpolator(new DecelerateInterpolator(ANIMATE_IN_INTERPOLATE_FACTOR));
+ }
+} \ No newline at end of file
diff --git a/src/org/cyanogenmod/theme/chooser/ThemeFragment.java b/src/org/cyanogenmod/theme/chooser/ThemeFragment.java
new file mode 100644
index 0000000..d0891a7
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/ThemeFragment.java
@@ -0,0 +1,3059 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.IntEvaluator;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.WallpaperManager;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.ThemeConfig;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.NinePatchDrawable;
+import android.media.MediaPlayer;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.Loader;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.MutableLong;
+import android.util.SparseArray;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewPropertyAnimator;
+import android.view.ViewTreeObserver;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.ScaleAnimation;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.ProgressBar;
+import android.widget.Space;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.cyanogenmod.theme.chooser.ComponentSelector.OnItemClickedListener;
+import org.cyanogenmod.theme.util.AudioUtils;
+import org.cyanogenmod.theme.util.BootAnimationHelper;
+import org.cyanogenmod.theme.util.CursorLoaderHelper;
+import org.cyanogenmod.theme.util.IconPreviewHelper;
+import org.cyanogenmod.theme.util.PreferenceUtils;
+import org.cyanogenmod.theme.util.ThemedTypefaceHelper;
+import org.cyanogenmod.theme.util.TypefaceHelperCache;
+import org.cyanogenmod.theme.util.Utils;
+import org.cyanogenmod.theme.util.WallpaperUtils;
+import org.cyanogenmod.theme.widget.BootAniImageView;
+import org.cyanogenmod.theme.widget.ConfirmCancelOverlay;
+import org.cyanogenmod.theme.widget.LockableScrollView;
+import org.cyanogenmod.theme.widget.ThemeTagLayout;
+
+import cyanogenmod.app.ThemeVersion;
+import cyanogenmod.providers.CMSettings;
+import cyanogenmod.providers.ThemesContract.PreviewColumns;
+import cyanogenmod.providers.ThemesContract.ThemesColumns;
+import cyanogenmod.themes.ThemeChangeRequest;
+import cyanogenmod.themes.ThemeChangeRequest.RequestType;
+import cyanogenmod.themes.ThemeManager;
+
+import org.cyanogenmod.internal.util.CmLockPatternUtils;
+import org.cyanogenmod.internal.util.ThemeUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.ZipFile;
+
+import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_ALARMS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_BOOT_ANIM;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_LAUNCHER;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_LOCKSCREEN;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_NOTIFICATIONS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_OVERLAYS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_RINGTONES;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_STATUS_BAR;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_NAVIGATION_BAR;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_ICONS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_FONTS;
+
+import static org.cyanogenmod.theme.chooser.ComponentSelector.DEFAULT_COMPONENT_ID;
+
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_INVALID;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_ALL;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_STATUS_BAR;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_FONT;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_ICONS;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_WALLPAPER;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_NAVIGATION_BAR;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_LOCKSCREEN;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_STYLE;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_BOOT_ANIMATION;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_RINGTONE;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_NOTIFICATION;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_ALARM;
+import static org.cyanogenmod.theme.util.CursorLoaderHelper.LOADER_ID_LIVE_LOCK_SCREEN;
+
+import static cyanogenmod.providers.CMSettings.Secure.LIVE_LOCK_SCREEN_ENABLED;
+
+import static org.cyanogenmod.internal.util.ThemeUtils.SYSTEM_TARGET_API;
+
+public class ThemeFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>,
+ ThemeManager.ThemeChangeListener, ThemeManager.ThemeProcessingListener {
+ private static final String TAG = ThemeFragment.class.getSimpleName();
+
+ public static final int ANIMATE_START_DELAY = 200;
+ public static final int ANIMATE_DURATION = 300;
+ public static final int ANIMATE_INTERPOLATE_FACTOR = 3;
+ public static final int ANIMATE_COMPONENT_CHANGE_DURATION = 200;
+ public static final int ANIMATE_COMPONENT_ICON_DELAY = 50;
+ public static final int ANIMATE_PROGRESS_IN_DURATION = 500;
+ public static final int ANIMATE_TITLE_OUT_DURATION = 400;
+ public static final int ANIMATE_PROGRESS_OUT_DURATION = 400;
+ public static final int ANIMATE_TITLE_IN_DURATION = 500;
+ public static final int ANIMATE_APPLY_LAYOUT_DURATION = 300;
+
+ public static final String CURRENTLY_APPLIED_THEME = "currently_applied_theme";
+
+ private static final ComponentName COMPONENT_DIALER =
+ new ComponentName("com.android.dialer", "com.android.dialer.DialtactsActivity");
+ private static final ComponentName COMPONENT_DIALERNEXT =
+ new ComponentName("com.cyngn.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_CAMERA =
+ new ComponentName("com.android.camera2", "com.android.camera.CameraActivity");
+ private static final ComponentName COMPONENT_BROWSER =
+ new ComponentName("com.android.browser", "com.android.browser.BrowserActivity");
+ private static final ComponentName COMPONENT_SETTINGS =
+ new ComponentName("com.android.settings", "com.android.settings.Settings");
+ private static final ComponentName COMPONENT_CALENDAR =
+ new ComponentName("com.android.calendar", "com.android.calendar.AllInOneActivity");
+ private static final ComponentName COMPONENT_GALERY =
+ new ComponentName("com.android.gallery3d", "com.android.gallery3d.app.GalleryActivity");
+
+ private static final String CAMERA_NEXT_PACKAGE = "com.cyngn.cameranext";
+ private static final String DIALER_NEXT_PACKAGE = "com.cyngn.dialer";
+
+ private static final int ADDITIONAL_CONTENT_SPACE_ID = 123456;
+ private static final long SLIDE_CONTENT_ANIM_DURATION = 300L;
+ private static final long LOCK_SCREEN_CARD_SCROLL_ANIMATION_DURATION = 400;
+ private static final long SHOW_LOCK_SCREEN_CARD_DELAY = 500;
+
+ private static final int DEFAULT_WIFI_MARGIN = 0;
+ private static final int DEFAULT_CLOCK_COLOR = Color.WHITE;
+
+ protected static final String WALLPAPER_NONE = "";
+ protected static final String LOCKSCREEN_NONE = "";
+
+ protected static final String ARG_PACKAGE_NAME = "pkgName";
+ protected static final String ARG_COMPONENT_ID = "cmpntId";
+ protected static final String ARG_SKIP_LOADING_ANIM = "skipLoadingAnim";
+ protected static final String ARG_ANIMATE_TO_LOCK_SCREEN_CARD = "animateToLockScreenCard";
+
+ private static final String LLS_PACKAGE_NAME = "com.cyngn.lockscreen.live";
+ private static final String LLS_PROVIDER_NAME =
+ "com.cyngn.lockscreen.live.LockScreenProviderService";
+
+ private static final int PERMISSION_REQUEST = 100;
+
+ protected static ComponentName[] sIconComponents;
+
+ protected static TypefaceHelperCache sTypefaceHelperCache;
+
+ /**
+ * Maps the card's resource ID to a theme component
+ */
+ private final SparseArray<String> mCardIdsToComponentTypes = new SparseArray<String>();
+
+ protected String mPkgName;
+ protected Typeface mTypefaceNormal;
+ protected int mBatteryStyle;
+
+ protected LockableScrollView mScrollView;
+ protected ViewGroup mScrollContent;
+ protected ViewGroup mPreviewContent; // Contains icons, font, nav/status etc. Not wallpaper
+ protected View mLoadingView;
+
+ //Status Bar Views
+ protected ComponentCardView mStatusBarCard;
+ protected ImageView mBluetooth;
+ protected ImageView mWifi;
+ protected ImageView mSignal;
+ protected ImageView mBattery;
+ protected TextView mClock;
+
+ // Other Misc Preview Views
+ protected FrameLayout mShadowFrame;
+ protected ImageView mWallpaper;
+ protected ViewGroup mStatusBar;
+ protected TextView mFontPreview;
+ protected ComponentCardView mStyleCard;
+ protected ComponentCardView mFontCard;
+ protected ComponentCardView mIconCard;
+ protected ComponentCardView mBootAnimationCard;
+ protected BootAniImageView mBootAnimation;
+
+ // Nav Bar Views
+ protected ComponentCardView mNavBarCard;
+ protected ViewGroup mNavBar;
+ protected ImageView mBackButton;
+ protected ImageView mHomeButton;
+ protected ImageView mRecentButton;
+
+ // Title Card Views
+ protected ViewGroup mTitleCard;
+ protected ViewGroup mTitleLayout;
+ protected TextView mTitle;
+ protected TextView mAuthor;
+ protected ImageView mCustomize;
+ protected ImageView mOverflow;
+ protected ImageView mDelete;
+ protected ImageView mReset;
+ protected ProgressBar mProgress;
+
+ // Additional Card Views
+ protected LinearLayout mAdditionalCards;
+ protected WallpaperCardView mWallpaperCard;
+ protected WallpaperCardView mLockScreenCard;
+
+ // Style views
+ protected ImageView mStylePreview;
+
+ // Sound cards
+ protected ComponentCardView mRingtoneCard;
+ protected ImageView mRingtonePlayPause;
+ protected ComponentCardView mNotificationCard;
+ protected ImageView mNotificationPlayPause;
+ protected ComponentCardView mAlarmCard;
+ protected ImageView mAlarmPlayPause;
+ protected Map<ImageView, MediaPlayer> mMediaPlayers;
+
+ protected Handler mHandler;
+
+ protected int mActiveCardId = -1;
+ protected ComponentSelector mSelector;
+ // Supported components for the theme this fragment represents
+ protected Map<String, String> mSelectedComponentsMap = new HashMap<String, String>();
+ protected Long mSelectedWallpaperComponentId;
+ // Current system theme configuration as component -> pkgName
+ protected Map<String, String> mCurrentTheme = new HashMap<String, String>();
+ protected MutableLong mCurrentWallpaperComponentId = new MutableLong(DEFAULT_COMPONENT_ID);
+ // Set of components available in the base theme
+ protected HashSet<String> mBaseThemeSupportedComponents = new HashSet<String>();
+ protected Cursor mCurrentCursor;
+ protected int mCurrentLoaderId;
+ protected boolean mThemeResetting;
+ protected boolean mSkipLoadingAnim;
+
+ // Accept/Cancel overlay
+ protected ConfirmCancelOverlay mConfirmCancelOverlay;
+
+ // Customize/Reset theme layout
+ protected View mCustomizeResetLayout;
+ protected View mResetButton;
+ protected View mCustomizeButton;
+ protected View mDismissButton;
+
+ // Processing theme layout
+ protected View mProcessingThemeLayout;
+
+ protected ThemeTagLayout mThemeTagLayout;
+
+ protected View mClickableView;
+ protected String mBaseThemePkgName;
+
+ protected Uri mExternalWallpaperUri;
+ protected Uri mExternalLockscreenUri;
+
+ protected boolean mExpanded;
+ protected boolean mProcessingResources;
+ protected boolean mApplyThemeOnPopulated;
+
+ protected boolean mIsLegacyTheme;
+
+ private Runnable mAfterPermissionGrantedRunnable;
+
+ private static final int mThemeVersion = ThemeVersion.getVersion();
+
+ protected boolean mShowLockScreenSelectorAfterContentLoaded;
+
+ protected enum CustomizeResetAction {
+ Customize,
+ Reset,
+ Dismiss
+ }
+
+ static ThemeFragment newInstance(String pkgName, boolean skipLoadingAnim) {
+ ThemeFragment f = new ThemeFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_PACKAGE_NAME, pkgName);
+ args.putBoolean(ARG_SKIP_LOADING_ANIM, skipLoadingAnim);
+ args.putLong(ARG_COMPONENT_ID, DEFAULT_COMPONENT_ID);
+ f.setArguments(args);
+ return f;
+ }
+
+ /**
+ * When creating, retrieve this instance's number from its arguments.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final Context context = getActivity();
+ mPkgName = getArguments().getString(ARG_PACKAGE_NAME);
+ mSkipLoadingAnim = getArguments().getBoolean(ARG_SKIP_LOADING_ANIM);
+ // TODO: Load from settings once available
+ mBatteryStyle = 0;/*Settings.System.getInt(context.getContentResolver(),
+ Settings.System.STATUS_BAR_BATTERY_STYLE, 0);*/
+
+ getIconComponents(context);
+ if (sTypefaceHelperCache == null) {
+ sTypefaceHelperCache = TypefaceHelperCache.getInstance();
+ }
+ ThemedTypefaceHelper helper = sTypefaceHelperCache.getHelperForTheme(context, mPkgName);
+ mTypefaceNormal = helper.getTypeface(Typeface.NORMAL);
+
+ mHandler = new Handler();
+
+ mCardIdsToComponentTypes.put(R.id.status_bar_container, MODIFIES_STATUS_BAR);
+ mCardIdsToComponentTypes.put(R.id.font_preview_container, MODIFIES_FONTS);
+ mCardIdsToComponentTypes.put(R.id.icon_container, MODIFIES_ICONS);
+ mCardIdsToComponentTypes.put(R.id.navigation_bar_container, MODIFIES_NAVIGATION_BAR);
+ mCardIdsToComponentTypes.put(R.id.wallpaper_card, MODIFIES_LAUNCHER);
+ mCardIdsToComponentTypes.put(R.id.lockscreen_card, MODIFIES_LOCKSCREEN);
+ mCardIdsToComponentTypes.put(R.id.style_card, MODIFIES_OVERLAYS);
+ mCardIdsToComponentTypes.put(R.id.bootani_preview_container, MODIFIES_BOOT_ANIM);
+ mCardIdsToComponentTypes.put(R.id.ringtone_preview_container, MODIFIES_RINGTONES);
+ mCardIdsToComponentTypes.put(R.id.notification_preview_container, MODIFIES_NOTIFICATIONS);
+ mCardIdsToComponentTypes.put(R.id.alarm_preview_container, MODIFIES_ALARMS);
+
+ mMediaPlayers = new HashMap<ImageView, MediaPlayer>(3);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_pager_list, container, false);
+
+ mScrollView = (LockableScrollView) v.findViewById(android.R.id.list);
+ mScrollView.setScrollingEnabled(false);
+ mScrollContent = (ViewGroup) mScrollView.getChildAt(0);
+ mPreviewContent = (ViewGroup) v.findViewById(R.id.preview_container);
+ mLoadingView = v.findViewById(R.id.loading_view);
+ mThemeTagLayout = (ThemeTagLayout) v.findViewById(R.id.tag_layout);
+
+ // Status Bar
+ mStatusBarCard = (ComponentCardView) v.findViewById(R.id.status_bar_container);
+ mStatusBar = (ViewGroup) v.findViewById(R.id.status_bar);
+ mBluetooth = (ImageView) v.findViewById(R.id.bluetooth_icon);
+ mWifi = (ImageView) v.findViewById(R.id.wifi_icon);
+ mSignal = (ImageView) v.findViewById(R.id.signal_icon);
+ mBattery = (ImageView) v.findViewById(R.id.battery);
+ mClock = (TextView) v.findViewById(R.id.clock);
+
+ // Wallpaper / Font / Icons / etc
+ mWallpaper = (ImageView) v.findViewById(R.id.wallpaper);
+ mFontCard = (ComponentCardView) v.findViewById(R.id.font_preview_container);
+ mFontPreview = (TextView) v.findViewById(R.id.font_preview);
+ mFontPreview.setTypeface(mTypefaceNormal);
+ mIconCard = (ComponentCardView) v.findViewById(R.id.icon_container);
+ mShadowFrame = (FrameLayout) v.findViewById(R.id.shadow_frame);
+ mStyleCard = (ComponentCardView) v.findViewById(R.id.style_card);
+ mStylePreview = (ImageView) v.findViewById(R.id.style_preview);
+ mBootAnimationCard = (ComponentCardView) v.findViewById(R.id.bootani_preview_container);
+ mBootAnimation =
+ (BootAniImageView) mBootAnimationCard.findViewById(R.id.bootani_preview);
+ mRingtoneCard = (ComponentCardView) v.findViewById(R.id.ringtone_preview_container);
+ mRingtonePlayPause = (ImageView) mRingtoneCard.findViewById(R.id.play_pause);
+ mNotificationCard = (ComponentCardView) v.findViewById(R.id.notification_preview_container);
+ mNotificationPlayPause = (ImageView) mNotificationCard.findViewById(R.id.play_pause);
+ mAlarmCard = (ComponentCardView) v.findViewById(R.id.alarm_preview_container);
+ mAlarmPlayPause = (ImageView) mAlarmCard.findViewById(R.id.play_pause);
+
+ // Nav Bar
+ mNavBarCard = (ComponentCardView) v.findViewById(R.id.navigation_bar_container);
+ mNavBar = (ViewGroup) v.findViewById(R.id.navigation_bar);
+ mBackButton = (ImageView) v.findViewById(R.id.back_button);
+ mHomeButton = (ImageView) v.findViewById(R.id.home_button);
+ mRecentButton = (ImageView) v.findViewById(R.id.recent_button);
+
+ // Title Card
+ mTitleCard = (ViewGroup)v.findViewById(R.id.title_card);
+ mTitleLayout = (ViewGroup) v.findViewById(R.id.title_layout);
+ mTitle = (TextView) v.findViewById(R.id.title);
+ mAuthor = (TextView) v.findViewById(R.id.author);
+ mProgress = (ProgressBar) v.findViewById(R.id.apply_progress);
+ mOverflow = (ImageView) v.findViewById(R.id.overflow);
+ mOverflow.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (isShowingConfirmCancelOverlay()) {
+ hideConfirmCancelOverlay();
+ } else if (isShowingCustomizeResetLayout()) {
+ hideCustomizeResetLayout();
+ }
+
+ PopupMenu popupmenu = new PopupMenu(getActivity(), mTitleCard, Gravity.END);
+ popupmenu.getMenuInflater().inflate(R.menu.overflow, popupmenu.getMenu());
+
+ Menu menu = popupmenu.getMenu();
+ if (CURRENTLY_APPLIED_THEME.equals(mPkgName) ||
+ mPkgName.equals(Utils.getDefaultThemePackageName(getActivity())) ||
+ mPkgName.equals(ThemeConfig.SYSTEM_DEFAULT)) {
+ menu.findItem(R.id.menu_delete).setEnabled(false);
+ }
+ if (!mThemeTagLayout.isCustomizedTagEnabled()) {
+ menu.findItem(R.id.menu_reset).setVisible(false);
+ }
+
+ popupmenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ return onPopupMenuItemClick(item);
+ }
+ });
+ popupmenu.show();
+ }
+ });
+ mCustomize = (ImageView) v.findViewById(R.id.customize);
+ mCustomize.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ if (!isShowingConfirmCancelOverlay() && !isShowingCustomizeResetLayout()) {
+ getChooserActivity().expand();
+ }
+ }
+ });
+
+ mDelete = (ImageView) v.findViewById(R.id.delete);
+ mDelete.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showDeleteThemeOverlay();
+ }
+ });
+ if (Utils.getDefaultThemePackageName(getActivity()).equals(mPkgName) ||
+ ThemeConfig.SYSTEM_DEFAULT.equals(mPkgName)) {
+ mDelete.setVisibility(View.GONE);
+ }
+
+ mReset = (ImageView) v.findViewById(R.id.reset);
+ mReset.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showResetThemeOverlay();
+ }
+ });
+ mReset.setVisibility(View.GONE);
+
+ if (!Utils.hasNavigationBar(getActivity())) {
+ adjustScrollViewPaddingTop();
+ mNavBarCard.setVisibility(View.GONE);
+ }
+
+ // Additional cards which should hang out offscreen until expanded
+ mAdditionalCards = (LinearLayout) v.findViewById(R.id.additional_cards);
+
+ mWallpaperCard = (WallpaperCardView) v.findViewById(R.id.wallpaper_card);
+ mLockScreenCard = (WallpaperCardView) v.findViewById(R.id.lockscreen_card);
+ int translationY = getDistanceToMoveBelowScreen(mAdditionalCards);
+ mAdditionalCards.setTranslationY(translationY);
+
+ mConfirmCancelOverlay = (ConfirmCancelOverlay) v.findViewById(R.id.confirm_cancel_overlay);
+ mClickableView = v.findViewById(R.id.clickable_view);
+
+ mCustomizeResetLayout = v.findViewById(R.id.customize_reset_theme_layout);
+ mDismissButton = mCustomizeResetLayout.findViewById(R.id.btn_dismiss);
+ mDismissButton.setOnClickListener(mCustomizeResetClickListener);
+ mResetButton = mCustomizeResetLayout.findViewById(R.id.btn_reset);
+ mResetButton.setOnClickListener(mCustomizeResetClickListener);
+ mCustomizeButton = mCustomizeResetLayout.findViewById(R.id.btn_customize);
+ mCustomizeButton.setOnClickListener(mCustomizeResetClickListener);
+
+ mProcessingThemeLayout = v.findViewById(R.id.processing_theme_layout);
+
+ if (mPkgName.equals(Utils.getDefaultThemePackageName(getActivity()))) {
+ mThemeTagLayout.setDefaultTagEnabled(true);
+ }
+ if (PreferenceUtils.hasThemeBeenUpdated(getActivity(), mPkgName)) {
+ mThemeTagLayout.setUpdatedTagEnabled(true);
+ }
+
+ if (mSkipLoadingAnim) {
+ mLoadingView.setVisibility(View.GONE);
+ mTitleLayout.setAlpha(1f);
+ }
+
+ getLoaderManager().initLoader(LOADER_ID_ALL, null, this);
+
+ setupCardClickListeners(v);
+
+ return v;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ stopMediaPlayers();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ThemeManager tm = getThemeManager();
+ if (tm != null) {
+ if (isThemeProcessing()) {
+ tm.registerProcessingListener(this);
+ mProcessingThemeLayout.setVisibility(View.VISIBLE);
+ mCustomize.setVisibility(View.INVISIBLE);
+ mCustomize.setAlpha(0f);
+ if (mDelete.getVisibility() != View.GONE) {
+ mDelete.setVisibility(View.INVISIBLE);
+ mDelete.setAlpha(0f);
+ }
+ mProcessingResources = true;
+ } else {
+ mCustomize.setVisibility(View.VISIBLE);
+ mCustomize.setAlpha(1f);
+ if (mDelete.getVisibility() != View.GONE) {
+ mDelete.setVisibility(View.VISIBLE);
+ mDelete.setAlpha(1f);
+ }
+ mProcessingResources = false;
+ }
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ freeMediaPlayers();
+ ThemeManager tm = getThemeManager();
+ if (tm != null) {
+ tm.removeClient(this);
+ tm.unregisterProcessingListener(this);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ int[] grantResults) {
+ if (requestCode == PERMISSION_REQUEST) {
+ int N = permissions.length;
+ for (int i = 0; i < N; i++) {
+ if (READ_EXTERNAL_STORAGE.equals(permissions[i])) {
+ if (grantResults[i] == PERMISSION_GRANTED) {
+ // Run the runnable now that we have been granted permission
+ if (mAfterPermissionGrantedRunnable != null) {
+ mAfterPermissionGrantedRunnable.run();
+ mAfterPermissionGrantedRunnable = null;
+ }
+ } else {
+ // inform the user that they will be unable to pick an image because
+ // we were not granted permission to do so
+ Toast.makeText(getActivity(),
+ R.string.read_external_permission_denied_message,
+ Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onProgress(int progress) {
+ mProgress.setProgress(progress);
+ }
+
+ private void setLiveLockScreenAsKeyguard(boolean setLLS) {
+ ComponentName cn = null;
+ if (setLLS) {
+ try {
+ final String[] permissions = Utils.getDangerousPermissionsNotGranted(getActivity(),
+ LLS_PACKAGE_NAME);
+ if (permissions.length > 0) {
+ Intent reqIntent = Utils.buildPermissionGrantRequestIntent(getActivity(),
+ LLS_PACKAGE_NAME, permissions);
+ if (reqIntent != null) {
+ startActivity(reqIntent);
+ }
+ }
+ cn = new ComponentName(LLS_PACKAGE_NAME, LLS_PROVIDER_NAME);
+ } catch (InvalidParameterException e) {
+ Log.e(TAG, "Package Manager couldn't find package " + LLS_PACKAGE_NAME, e);
+ return;
+ }
+ }
+
+ CmLockPatternUtils lockPatternUtils = new CmLockPatternUtils(getActivity());
+ try {
+ lockPatternUtils.setThirdPartyKeyguard(cn);
+ } catch (PackageManager.NameNotFoundException e) {
+ // we should not be here!
+ }
+ }
+
+ @Override
+ public void onFinish(boolean isSuccess) {
+ // We post a runnable to mHandler so the client is removed from the same thread
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ ThemeManager tm = getThemeManager();
+ if (tm != null) tm.removeClient(ThemeFragment.this);
+ }
+ });
+ if (isSuccess) {
+ if (mExternalLockscreenUri != null) {
+ // Handle setting an external wallpaper in a separate thread
+ // Need to do this AFTER ThemeMgr is done processing our change request.
+ // The external lock screen that we just applied would be removed when
+ // the change request is setting/clearing the lock screen
+ new Thread(mApplyExternalLockscreenRunnable).start();
+ }
+ Map<String, String> appliedComponents = getComponentsToApply();
+ boolean modLLS = appliedComponents.containsKey(MODIFIES_LIVE_LOCK_SCREEN);
+ if (modLLS) {
+ String pkgName = appliedComponents.get(MODIFIES_LIVE_LOCK_SCREEN);
+ if (pkgName.equals(LOCKSCREEN_NONE)) {
+ setLiveLockScreenAsKeyguard(false);
+ } else {
+ setLiveLockScreenAsKeyguard(true);
+ }
+ }
+ mProgress.setProgress(100);
+ animateProgressOut();
+ }
+ getChooserActivity().themeChangeEnd(isSuccess);
+ }
+
+ @Override
+ public void onFinishedProcessing(String pkgName) {
+ if (pkgName.equals(mPkgName) || pkgName.equals(mBaseThemePkgName)) {
+ ThemeManager tm = getThemeManager();
+ if (tm != null) {
+ tm.unregisterProcessingListener(this);
+ }
+ }
+ }
+
+ @Override
+ public void setUserVisibleHint(boolean isVisibleToUser) {
+ super.setUserVisibleHint(isVisibleToUser);
+ if (mThemeTagLayout == null) return;
+
+ if (!isVisibleToUser) {
+ if (PreferenceUtils.hasThemeBeenUpdated(getActivity(), mPkgName)) {
+ mThemeTagLayout.setUpdatedTagEnabled(true);
+ }
+ } else {
+ if (PreferenceUtils.hasThemeBeenUpdated(getActivity(), mPkgName)) {
+ PreferenceUtils.removeUpdatedTheme(getActivity(), mPkgName);
+ }
+ }
+ }
+
+ public void setWallpaperImageUri(Uri uri) {
+ mExternalWallpaperUri = uri;
+ final Point size = new Point(mWallpaper.getWidth(), mWallpaper.getHeight());
+ final Drawable wp = getWallpaperDrawableFromUri(uri, size);
+ mWallpaperCard.setWallpaper(wp);
+ mWallpaper.setImageDrawable(wp);
+ // remove the entry from mSelectedComponentsMap
+ mSelectedComponentsMap.remove(ThemesColumns.MODIFIES_LAUNCHER);
+ }
+
+ public void setLockscreenImageUri(Uri uri) {
+ mExternalLockscreenUri = uri;
+ final Point size = new Point(mLockScreenCard.getWidth(), mLockScreenCard.getHeight());
+ final Drawable wp = getWallpaperDrawableFromUri(uri, size);
+ if (mLockScreenCard.isShowingEmptyView()) {
+ mLockScreenCard.setEmptyViewEnabled(false);
+ }
+ mLockScreenCard.setWallpaper(wp);
+ // remove the entry from mSelectedComponentsMap
+ mSelectedComponentsMap.remove(ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN);
+ mSelectedComponentsMap.remove(ThemesColumns.MODIFIES_LOCKSCREEN);
+ }
+
+ protected Drawable getWallpaperDrawableFromUri(Uri uri, Point size) {
+ final Context context = getActivity();
+ final Resources res = context.getResources();
+ Bitmap bmp = WallpaperUtils.createPreview(size, context, uri, null, res, 0, 0, false);
+ if (bmp != null) {
+ return new BitmapDrawable(res, bmp);
+ }
+ return null;
+ }
+
+ protected ChooserActivity getChooserActivity() {
+ return (ChooserActivity) getActivity();
+ }
+
+ private void adjustScrollViewPaddingTop() {
+ Resources res = getResources();
+ int extraPadding =
+ res.getDimensionPixelSize(R.dimen.navigation_bar_height) / 2;
+ mScrollView.setPadding(mScrollView.getPaddingLeft(),
+ mScrollView.getPaddingTop() + extraPadding, mScrollView.getPaddingRight(),
+ mScrollView.getPaddingBottom());
+ }
+
+ protected boolean isThemeProcessing() {
+ ThemeManager tm = getThemeManager();
+ if (tm != null) {
+ final String pkgName = mBaseThemePkgName != null ? mBaseThemePkgName : mPkgName;
+ return tm.isThemeBeingProcessed(pkgName);
+ }
+ return false;
+ }
+
+ protected boolean onPopupMenuItemClick(MenuItem item) {
+ switch(item.getItemId()) {
+ /* TODO: Add back in once there is UX available for this feature
+ case R.id.menu_author:
+ Toast.makeText(getActivity(),
+ "Not supported",
+ Toast.LENGTH_LONG).show();
+ break;
+ */
+ case R.id.menu_delete:
+ showDeleteThemeOverlay();
+ break;
+ }
+
+ return true;
+ }
+
+ public void expand() {
+ if (mCurrentLoaderId == LOADER_ID_ALL && mCurrentCursor != null) {
+ loadAdditionalCards(mCurrentCursor);
+ // we don't need this now that the additional cards are loaded, and
+ // we don't want to re-load these cards if the we expand again.
+ mCurrentCursor = null;
+ }
+ mClickableView.setVisibility(View.GONE);
+ mScrollView.setScrollingEnabled(true);
+ // Full width and height!
+ ViewGroup content = (ViewGroup) mScrollView.getParent();
+ content.setPadding(0, 0, 0, 0);
+ ViewGroup.LayoutParams layoutParams = mPreviewContent.getLayoutParams();
+ layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
+ mPreviewContent.setLayoutParams(layoutParams);
+ mScrollView.setPadding(0,0,0,0);
+
+ // The parent of the wallpaper squishes the wp slightly because of padding from the 9 patch
+ // When the parent expands, the wallpaper returns to regular size which creates an
+ // undesireable effect.
+ Rect padding = new Rect();
+ NinePatchDrawable bg = (NinePatchDrawable) mShadowFrame.getBackground();
+ bg.getPadding(padding);
+ mIconCard.setPadding(padding.left, padding.top, padding.right, padding.bottom);
+ mShadowFrame.setBackground(null);
+ mShadowFrame.setPadding(0, 0, 0, 0);
+
+ // Off screen cards will become visible and then be animated in
+ mWallpaperCard.setVisibility(View.VISIBLE);
+
+ // Expand the children
+ int top = (int) getResources()
+ .getDimension(R.dimen.expanded_card_margin_top);
+ for (int i = 0; i < mPreviewContent.getChildCount(); i++) {
+ ComponentCardView child = (ComponentCardView) mPreviewContent.getChildAt(i);
+
+ LinearLayout.LayoutParams lparams =
+ (LinearLayout.LayoutParams) child.getLayoutParams();
+ if (child == mStatusBarCard) {
+ int statusBarHeight = getResources()
+ .getDimensionPixelSize(R.dimen.status_bar_height);
+ lparams.setMargins(0, top + statusBarHeight, 0, 0);
+ } else {
+ lparams.setMargins(0, top, 0, 0);
+ }
+
+ child.setLayoutParams(lparams);
+ child.expand(false);
+ }
+
+ // Expand the additional children.
+ mAdditionalCards.setVisibility(View.VISIBLE);
+ for (int i = 0; i < mAdditionalCards.getChildCount(); i++) {
+ View v = mAdditionalCards.getChildAt(i);
+ if (v instanceof ComponentCardView) {
+ ComponentCardView card = (ComponentCardView) v;
+ card.setVisibility(View.VISIBLE);
+ card.expand(true);
+ }
+ }
+
+ // Collect the present position of all the children. The next layout/draw cycle will
+ // change these bounds since we just expanded them. Then we can animate from prev location
+ // to the new location. Note that the order of these calls matter as they all
+ // add themselves to the root layout as overlays
+ mScrollView.requestLayout();
+ animateWallpaperOut();
+ animateTitleCard(true, false);
+ animateChildren(true, getChildrensGlobalBounds(mPreviewContent));
+ animateExtras(true);
+ mSelector = getChooserActivity().getComponentSelector();
+ mSelector.setOnItemClickedListener(mOnComponentItemClicked);
+ if (mBootAnimation != null) mBootAnimation.start();
+ hideThemeTagLayout();
+ mExpanded = true;
+ }
+
+
+
+ // Returns the boundaries for all the children of parent relative to the app window
+ private List<Rect> getChildrensGlobalBounds(ViewGroup parent) {
+ List<Rect> bounds = new ArrayList<Rect>();
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ final View v = parent.getChildAt(i);
+ int[] pos = new int[2];
+ v.getLocationInWindow(pos);
+ Rect boundary = new Rect(pos[0], pos[1], pos[0] + v.getWidth(), pos[1]+v.getHeight());
+ bounds.add(boundary);
+ }
+ return bounds;
+ }
+
+ public void performClick(boolean clickedOnContent) {
+ // Don't do anything if the theme is being processed
+ if (mProcessingThemeLayout.getVisibility() == View.VISIBLE) return;
+
+ if (clickedOnContent) {
+ showApplyThemeOverlay();
+ } else {
+ if (isShowingConfirmCancelOverlay()) {
+ hideConfirmCancelOverlay();
+ }
+ }
+ }
+
+ public void fadeOutCards(Runnable endAction) {
+ for (int i = 0; i < mPreviewContent.getChildCount(); i++) {
+ ComponentCardView v = (ComponentCardView) mPreviewContent.getChildAt(i);
+ v.animateFadeOut();
+ }
+ mHandler.postDelayed(endAction, ComponentCardView.CARD_FADE_DURATION);
+ }
+
+ public void collapse(final boolean applyTheme) {
+ mScrollView.setScrollingEnabled(false);
+
+ // Pad the view so it appears thinner
+ ViewGroup content = (ViewGroup) mScrollView.getParent();
+ Resources r = mScrollView.getContext().getResources();
+ int leftRightPadding = (int) r.getDimension(R.dimen.collapsed_theme_page_padding);
+ content.setPadding(leftRightPadding, 0, leftRightPadding, 0);
+
+ if (applyTheme) {
+ final boolean customized = isThemeCustomized();
+ mThemeTagLayout.setCustomizedTagEnabled(customized);
+ mReset.setVisibility(customized ? View.VISIBLE : View.GONE);
+ }
+
+ //Move the theme preview so that it is near the center of page per spec
+ int paddingTop = (int) r.getDimension(R.dimen.collapsed_theme_page_padding_top);
+ if (!Utils.hasNavigationBar(getActivity())) {
+ paddingTop +=
+ r.getDimensionPixelSize(R.dimen.navigation_bar_height) / 2;
+ }
+ mScrollView.setPadding(0, paddingTop, 0, 0);
+
+ // During expand the wallpaper size decreases slightly to makeup for 9patch padding
+ // so when we collapse we should increase it again.
+ mShadowFrame.setBackgroundResource(R.drawable.bg_themepreview_shadow);
+ Rect padding = new Rect();
+ final NinePatchDrawable bg = (NinePatchDrawable) mShadowFrame.getBackground();
+ bg.getPadding(padding);
+ mShadowFrame.setPadding(padding.left, padding.top, padding.right, padding.bottom);
+
+ // Gradually fade the drop shadow back in or else it will be out of place
+ ValueAnimator shadowAnimation = ValueAnimator.ofObject(new IntEvaluator(), 0, 255);
+ shadowAnimation.setDuration(ANIMATE_DURATION);
+ shadowAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ bg.setAlpha((Integer) animator.getAnimatedValue());
+ }
+
+ });
+ shadowAnimation.start();
+
+ //Move the title card back in
+ mTitleCard.setVisibility(View.VISIBLE);
+ mTitleCard.setTranslationY(0);
+
+ // Shrink the height
+ ViewGroup.LayoutParams layoutParams = mPreviewContent.getLayoutParams();
+ Resources resources = mPreviewContent.getResources();
+ layoutParams.height = (int) resources.getDimension(R.dimen.theme_preview_height);
+
+ mScrollView.requestLayout();
+ List<Rect> bounds = getChildrensGlobalBounds(mPreviewContent);
+ for (int i = 0; i < mPreviewContent.getChildCount(); i++) {
+ ComponentCardView child = (ComponentCardView) mPreviewContent.getChildAt(i);
+ LinearLayout.LayoutParams lparams =
+ (LinearLayout.LayoutParams) child.getLayoutParams();
+ lparams.setMargins(0, 0, 0, 0);
+
+ if (child.getId() == R.id.icon_container) {
+ int top = (int) child.getResources()
+ .getDimension(R.dimen.collapsed_icon_card_margin_top);
+ lparams.setMargins(0, top, 0, 0);
+ } else if (child.getId() == R.id.font_preview_container) {
+ int top = (int) child.getResources()
+ .getDimension(R.dimen.collapsed_font_card_margin_top);
+ lparams.setMargins(0, top, 0, 0);
+ } else if (child.getId() == R.id.navigation_bar_container) {
+ int top = (int) child.getResources()
+ .getDimension(R.dimen.collapsed_navbar_card_margin_top);
+ lparams.setMargins(0, top, 0, 0);
+ }
+
+ child.getLayoutParams();
+ child.collapse();
+ }
+
+ // Collapse additional cards
+ for (int i = 0; i < mAdditionalCards.getChildCount(); i++) {
+ View v = mAdditionalCards.getChildAt(i);
+ if (v instanceof ComponentCardView) {
+ ComponentCardView card = (ComponentCardView) v;
+ card.setVisibility(View.VISIBLE);
+ card.collapse();
+ }
+ }
+
+ animateChildren(false, bounds);
+ animateExtras(false);
+ animateWallpaperIn();
+ animateTitleCard(false, applyTheme);
+ if (mBootAnimation != null) mBootAnimation.stop();
+ stopMediaPlayers();
+ showThemeTagLayout();
+
+ // Need to set the wallpaper background to black if the user has selected to apply
+ // the "none" wallpaper
+ if (applyTheme) {
+ String pkgName = mSelectedComponentsMap.get(ThemesColumns.MODIFIES_LAUNCHER);
+ if (pkgName != null && pkgName.length() == 0) {
+ mWallpaper.setImageResource(R.drawable.wallpaper_none_bg);
+ }
+ // we do this here instead of in applyTheme() because this can take a bit longer
+ // to propagate the change from WallpaperManager back to us
+ if (mExternalWallpaperUri != null) {
+ // Handle setting an external wallpaper in a separate thread
+ new Thread(mApplyExternalWallpaperRunnable).start();
+ }
+ }
+ mExpanded = false;
+ }
+
+ // This will animate the children's vertical positions between the previous bounds and the
+ // new bounds which occur on the next draw
+ private void animateChildren(final boolean isExpanding, final List<Rect> prevBounds) {
+ final ViewGroup root = (ViewGroup) getActivity().getWindow()
+ .getDecorView().findViewById(android.R.id.content);
+
+ final Resources res = getResources();
+ final float yOffset =
+ res.getDimensionPixelSize(R.dimen.expand_collapse_child_offset)
+ * (isExpanding ? -1 : 1);
+ // Grab the child's new location and animate from prev to current loc.
+ final ViewTreeObserver observer = mScrollContent.getViewTreeObserver();
+ observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ public boolean onPreDraw() {
+ observer.removeOnPreDrawListener(this);
+
+ for (int i = mPreviewContent.getChildCount() - 1; i >= 0; i--) {
+ final ComponentCardView v = (ComponentCardView) mPreviewContent.getChildAt(i);
+
+ float prevY;
+ float endY;
+ float prevHeight;
+ float endHeight;
+ if (i >= prevBounds.size()) {
+ // View is being created
+ prevY = mPreviewContent.getTop() + mPreviewContent.getHeight();
+ endY = v.getY();
+ prevHeight = v.getHeight();
+ endHeight = v.getHeight();
+ } else {
+ Rect boundary = prevBounds.get(i);
+ prevY = boundary.top;
+ prevHeight = boundary.height();
+
+ int[] endPos = new int[2];
+ v.getLocationInWindow(endPos);
+ endY = endPos[1];
+ endHeight = v.getHeight();
+ }
+
+ int paddingTop = v.getPaddingTop() / 2;
+ float dy = (prevY - endY - paddingTop) + (prevHeight - endHeight) / 2;
+ dy += yOffset;
+ v.setTranslationY(dy);
+ root.getOverlay().add(v);
+
+ // Expanding has a delay while the wallpaper begins to fade out
+ // Collapsing is opposite of this so wallpaper will have the delay instead
+ int startDelay = isExpanding ? ANIMATE_START_DELAY : 0;
+
+ v.animate()
+ .setStartDelay(startDelay)
+ .translationY(0)
+ .setDuration(ANIMATE_DURATION)
+ .setInterpolator(
+ new DecelerateInterpolator(ANIMATE_INTERPOLATE_FACTOR))
+ .withEndAction(new Runnable() {
+ public void run() {
+ root.getOverlay().remove(v);
+ mPreviewContent.addView(v, 0);
+ }
+ });
+ v.postDelayed(new Runnable() {
+ public void run() {
+ if (isExpanding) {
+ v.animateExpand();
+ }
+ }
+ }, ANIMATE_DURATION / 2);
+ }
+ return true;
+ }
+ });
+ }
+
+ private void animateExtras(final boolean isExpanding) {
+ int[] pos = new int[2];
+ mAdditionalCards.getLocationInWindow(pos);
+ final ViewGroup parent = (ViewGroup) mAdditionalCards.getParent();
+ final ViewGroup root = (ViewGroup) getActivity().getWindow()
+ .getDecorView().findViewById(android.R.id.content);
+
+ // During a collapse we don't want the card to shrink so add it to the overlay now
+ // During an expand we want the card to expand so add it to the overlay post-layout
+ if (!isExpanding) {
+ root.getOverlay().add(mAdditionalCards);
+ }
+
+ // Expanding has a delay while the wallpaper begins to fade out
+ // Collapsing is opposite of this so wallpaper will have the delay instead
+ final int startDelay = isExpanding ? ANIMATE_START_DELAY : 0;
+ final ViewTreeObserver observer = mScrollContent.getViewTreeObserver();
+ observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ public boolean onPreDraw() {
+ observer.removeOnPreDrawListener(this);
+
+ int translationY = 0;
+ if (isExpanding) {
+ root.getOverlay().add(mAdditionalCards);
+ } else {
+ translationY = getDistanceToMoveBelowScreen(mAdditionalCards);
+ }
+
+ int duration = isExpanding ? ANIMATE_DURATION + 100 : ANIMATE_DURATION;
+ mAdditionalCards.animate()
+ .setStartDelay(startDelay)
+ .translationY(translationY)
+ .setDuration(duration)
+ .setInterpolator(
+ new DecelerateInterpolator(ANIMATE_INTERPOLATE_FACTOR))
+ .withEndAction(new Runnable() {
+ public void run() {
+ if (!isExpanding) {
+ mAdditionalCards.setVisibility(View.INVISIBLE);
+ }
+ root.getOverlay().remove(mAdditionalCards);
+ parent.addView(mAdditionalCards);
+ }
+ });
+ return false;
+ }
+ });
+ }
+
+ private int getDistanceToMoveBelowScreen(View v) {
+ Display display = getActivity().getWindowManager().getDefaultDisplay();
+ Point p = new Point();
+ display.getSize(p);
+ int heightId = getResources()
+ .getIdentifier("navigation_bar_height", "dimen", "android");
+ int navbar_height = getResources().getDimensionPixelSize(heightId);
+ int[] pos = new int[2];
+ v.getLocationInWindow(pos);
+ return p.y + navbar_height - pos[1];
+ }
+
+ private void animateTitleCard(final boolean expand, final boolean applyTheme) {
+ final ViewGroup parent = (ViewGroup) mTitleCard.getParent();
+ // Get current location of the title card
+ int[] location = new int[2];
+ mTitleCard.getLocationOnScreen(location);
+ final int prevY = location[1];
+ final int position = parent.indexOfChild(mTitleCard);
+
+ final ViewTreeObserver observer = mScrollContent.getViewTreeObserver();
+ observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ public boolean onPreDraw() {
+ observer.removeOnPreDrawListener(this);
+
+ final ViewGroup root = (ViewGroup) getActivity().getWindow()
+ .getDecorView().findViewById(android.R.id.content);
+
+ root.getOverlay().add(mTitleCard);
+
+ //Move title card back where it was before the relayout
+ float alpha = 1f;
+ if (expand) {
+ int[] endPos = new int[2];
+ mTitleCard.getLocationInWindow(endPos);
+ int endY = endPos[1];
+ mTitleCard.setTranslationY(prevY - endY);
+ alpha = 0;
+ } else {
+ }
+
+ // Fade the title card and move it out of the way
+ mTitleCard.animate()
+ .alpha(alpha)
+ .setDuration(ANIMATE_DURATION)
+ .withEndAction(new Runnable() {
+ public void run() {
+ root.getOverlay().remove(mTitleCard);
+ parent.addView(mTitleCard, position);
+ if (expand) {
+ mTitleCard.setVisibility(View.INVISIBLE);
+ } else {
+ mTitleCard.setVisibility(View.VISIBLE);
+ mClickableView.setVisibility(View.VISIBLE);
+ if (applyTheme) {
+ // The title card is the last animation when collapsing so
+ // we will handle applying the theme, if applicable, here
+ applyTheme();
+ }
+ }
+ }
+ });
+ return true;
+ }
+ });
+ }
+
+ private void animateWallpaperOut() {
+ final ViewGroup root = (ViewGroup) getActivity().getWindow()
+ .getDecorView().findViewById(android.R.id.content);
+
+ int[] location = new int[2];
+ mWallpaper.getLocationOnScreen(location);
+
+ final int prevY = location[1];
+
+ final ViewTreeObserver observer = mScrollContent.getViewTreeObserver();
+ observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ public boolean onPreDraw() {
+ observer.removeOnPreDrawListener(this);
+ root.getOverlay().add(mWallpaper);
+
+ int[] location = new int[2];
+ mWallpaper.getLocationOnScreen(location);
+ final int newY = location[1];
+
+ mWallpaper.setTranslationY(prevY - newY);
+ mWallpaper.animate()
+ .alpha(0f)
+ .setDuration(300)
+ .withEndAction(new Runnable() {
+ public void run() {
+ root.getOverlay().remove(mWallpaper);
+ mShadowFrame.addView(mWallpaper, 0);
+ mWallpaper.setVisibility(View.GONE);
+ }
+ });
+ return true;
+
+ }
+ });
+ }
+
+ private void animateWallpaperIn() {
+ mWallpaper.setVisibility(View.VISIBLE);
+ mWallpaper.setTranslationY(0);
+ mWallpaper.animate()
+ .alpha(1f)
+ .setDuration(300);
+ }
+
+ protected String getAppliedFontPackageName() {
+ final Configuration config = getActivity().getResources().getConfiguration();
+ final ThemeConfig themeConfig = config != null ? config.themeConfig : null;
+ return themeConfig != null ? themeConfig.getFontPkgName() :
+ ThemeConfig.getSystemTheme().getFontPkgName();
+ }
+
+ protected ThemeManager getThemeManager() {
+ return ThemeManager.getInstance(getActivity());
+ }
+
+ private void freeMediaPlayers() {
+ for (MediaPlayer mp : mMediaPlayers.values()) {
+ if (mp != null) {
+ mp.stop();
+ mp.release();
+ }
+ }
+ mMediaPlayers.clear();
+ }
+
+ protected 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(R.drawable.media_sound_preview);
+ mp.pause();
+ mp.seekTo(0);
+ } else {
+ stopMediaPlayers();
+ ((ImageView) v).setImageResource(R.drawable.media_sound_stop);
+ mp.start();
+ }
+ }
+ }
+ };
+
+ protected MediaPlayer.OnCompletionListener mPlayCompletionListener
+ = new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ for (ImageView v : mMediaPlayers.keySet()) {
+ if (mp == mMediaPlayers.get(v)) {
+ if (v != null) {
+ v.setImageResource(R.drawable.media_sound_preview);
+ }
+ }
+ }
+ }
+ };
+
+ private void stopMediaPlayers() {
+ for (ImageView v : mMediaPlayers.keySet()) {
+ if (v != null) {
+ v.setImageResource(R.drawable.media_sound_preview);
+ }
+ MediaPlayer mp = mMediaPlayers.get(v);
+ if (mp != null && mp.isPlaying()) {
+ mp.pause();
+ mp.seekTo(0);
+ }
+ }
+ }
+
+ protected void resetTheme() {
+ mSelectedComponentsMap.clear();
+ Bundle args = new Bundle();
+ args.putString(ARG_PACKAGE_NAME, mBaseThemePkgName);
+ args.putLong(ARG_COMPONENT_ID, DEFAULT_COMPONENT_ID);
+ getLoaderManager().restartLoader(LOADER_ID_ALL, args, this);
+ mThemeResetting = true;
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ String pkgName = mPkgName;
+ long componentId = DEFAULT_COMPONENT_ID;
+ if (args != null) {
+ pkgName = args.getString(ARG_PACKAGE_NAME);
+ componentId = args.getLong(ARG_COMPONENT_ID, DEFAULT_COMPONENT_ID);
+ }
+ return CursorLoaderHelper.themeFragmentCursorLoader(getActivity(), id, pkgName,
+ componentId);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
+ if (c.getCount() == 0) return;
+ mCurrentCursor = c;
+ mCurrentLoaderId = loader.getId();
+ c.moveToFirst();
+ boolean animate = !mApplyThemeOnPopulated;
+ switch (mCurrentLoaderId) {
+ case LOADER_ID_ALL:
+ if (mProcessingResources && !isThemeProcessing()) {
+ mProcessingResources = false;
+ hideProcessingOverlay();
+ }
+ loadLegacyThemeInfo(c);
+ populateSupportedComponents(c);
+ loadWallpaper(c, false);
+ loadStatusBar(c, false);
+ loadIcons(c, false);
+ loadNavBar(c, false);
+ loadTitle(c);
+ loadFont(c, false);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ animateContentIn();
+ }
+ });
+ if (mShowLockScreenSelectorAfterContentLoaded) {
+ getChooserActivity().expandContentAndAnimateLockScreenCardIn();
+ mShowLockScreenSelectorAfterContentLoaded = false;
+ }
+ break;
+ case LOADER_ID_STATUS_BAR:
+ loadStatusBar(c, animate);
+ break;
+ case LOADER_ID_FONT:
+ loadFont(c, animate);
+ break;
+ case LOADER_ID_ICONS:
+ loadIcons(c, animate);
+ break;
+ case LOADER_ID_WALLPAPER:
+ loadWallpaper(c, animate);
+ break;
+ case LOADER_ID_NAVIGATION_BAR:
+ loadNavBar(c, animate);
+ break;
+ case LOADER_ID_LIVE_LOCK_SCREEN:
+ case LOADER_ID_LOCKSCREEN:
+ loadLockScreen(c, animate);
+ break;
+ case LOADER_ID_STYLE:
+ loadStyle(c, animate);
+ break;
+ case LOADER_ID_BOOT_ANIMATION:
+ loadBootAnimation(c);
+ break;
+ case LOADER_ID_RINGTONE:
+ loadAudible(RingtoneManager.TYPE_RINGTONE, c, animate);
+ break;
+ case LOADER_ID_NOTIFICATION:
+ loadAudible(RingtoneManager.TYPE_NOTIFICATION, c, animate);
+ break;
+ case LOADER_ID_ALARM:
+ loadAudible(RingtoneManager.TYPE_ALARM, c, animate);
+ break;
+ }
+ if (mCurrentLoaderId != LOADER_ID_ALL) {
+ if (!componentsChanged()) {
+ getChooserActivity().hideSaveApplyButton();
+ } else if (!mApplyThemeOnPopulated) {
+ getChooserActivity().showSaveApplyButton();
+ }
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {}
+
+ private void loadAdditionalCards(Cursor c) {
+ for(int i=0; i < mAdditionalCards.getChildCount(); i++) {
+ View v = mAdditionalCards.getChildAt(i);
+ if (v instanceof ComponentCardView) {
+ String component = mCardIdsToComponentTypes.get(v.getId());
+ loadAdditionalCard(c, component, shouldShowComponentCard(component));
+ }
+ }
+ }
+
+ private void loadAdditionalCard(Cursor c, String component, boolean hasContent) {
+ if (MODIFIES_LOCKSCREEN.equals(component)) {
+ if (hasContent) {
+ loadLockScreen(c, false);
+ } else {
+ mLockScreenCard.clearWallpaper();
+ mLockScreenCard.setEmptyViewEnabled(true);
+ setAddComponentTitle(mLockScreenCard, getString(R.string.lockscreen_label));
+ }
+ } else if (MODIFIES_LAUNCHER.equals(component)) {
+ // this was already loaded so no need to do this again.
+ } else if (MODIFIES_OVERLAYS.equals(component)) {
+ if (hasContent) {
+ loadStyle(c, false);
+ } else {
+ mStyleCard.setEmptyViewEnabled(true);
+ setAddComponentTitle(mStyleCard,
+ getString(R.string.style_label));
+ }
+ } else if (MODIFIES_BOOT_ANIM.equals(component)) {
+ if (hasContent) {
+ loadBootAnimation(c);
+ } else {
+ mBootAnimationCard.setEmptyViewEnabled(true);
+ setAddComponentTitle(mBootAnimationCard,
+ getString(R.string.boot_animation_label));
+ }
+ } else if (MODIFIES_RINGTONES.equals(component)) {
+ if (hasContent) {
+ loadAudible(RingtoneManager.TYPE_RINGTONE, c, false);
+ } else {
+ mRingtoneCard.setEmptyViewEnabled(true);
+ setAddComponentTitle(mRingtoneCard,
+ getAudibleLabel(RingtoneManager.TYPE_RINGTONE));
+ }
+ } else if (MODIFIES_NOTIFICATIONS.equals(component)) {
+ if (hasContent) {
+ loadAudible(RingtoneManager.TYPE_NOTIFICATION, c, false);
+ } else {
+ mNotificationCard.setEmptyViewEnabled(true);
+ setAddComponentTitle(mNotificationCard,
+ getAudibleLabel(RingtoneManager.TYPE_NOTIFICATION));
+ }
+ } else if (MODIFIES_ALARMS.equals(component)) {
+ if (hasContent) {
+ loadAudible(RingtoneManager.TYPE_ALARM, c, false);
+ } else {
+ mAlarmCard.setEmptyViewEnabled(true);
+ setAddComponentTitle(mAlarmCard,
+ getAudibleLabel(RingtoneManager.TYPE_ALARM));
+ }
+ } else {
+ throw new IllegalArgumentException("Don't know how to load: " + component);
+ }
+ }
+
+ protected void populateSupportedComponents(Cursor c) {
+ List<String> components = ThemeUtils.getAllComponents();
+ for(String component : components) {
+ int pkgIdx = c.getColumnIndex(ThemesColumns.PKG_NAME);
+ int modifiesCompIdx = c.getColumnIndex(component);
+
+ String pkg = c.getString(pkgIdx);
+ boolean supported = (modifiesCompIdx >= 0) && (c.getInt(modifiesCompIdx) == 1);
+ if (supported) {
+ mBaseThemeSupportedComponents.add(component);
+ mSelectedComponentsMap.put(component, pkg);
+ }
+ }
+
+ if (mApplyThemeOnPopulated) {
+ applyTheme();
+ }
+ }
+
+ /**
+ * Determines whether a card should be shown or not.
+ * UX Rules:
+ * 1) "My Theme" always shows all cards
+ * 2) Other themes only show what has been implemented in the theme
+ *
+ */
+ protected Boolean shouldShowComponentCard(String component) {
+ String pkg = mSelectedComponentsMap.get(component);
+ return pkg != null && pkg.equals(mPkgName);
+ }
+
+ protected void loadLegacyThemeInfo(Cursor c) {
+ int targetApiIdx = c.getColumnIndex(ThemesColumns.TARGET_API);
+ // If this is being called for a MyThemeFragment the index will be -1 so set to
+ // SYSTEM_TARGET_API so we don't display the tag. If the user applied a legacy theme
+ // then they should have already been warned.
+ int targetApi = targetApiIdx < 0 ? SYSTEM_TARGET_API : c.getInt(targetApiIdx);
+ mIsLegacyTheme = targetApi != SYSTEM_TARGET_API && targetApi <= Build.VERSION_CODES.KITKAT;
+ mThemeTagLayout.setLegacyTagEnabled(mIsLegacyTheme);
+ }
+
+ protected void loadTitle(Cursor c) {
+ int titleIdx = c.getColumnIndex(ThemesColumns.TITLE);
+ int authorIdx = c.getColumnIndex(ThemesColumns.AUTHOR);
+ mTitle.setText(c.getString(titleIdx));
+ mAuthor.setText(c.getString(authorIdx));
+ }
+
+ protected void loadWallpaper(Cursor c, boolean animate) {
+ mExternalWallpaperUri = null;
+ Drawable overlay = null;
+ if (animate) {
+ overlay = getOverlayDrawable(mWallpaperCard, true);
+ }
+ if (mWallpaperCard.isShowingEmptyView()) mWallpaperCard.setEmptyViewEnabled(false);
+
+ int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME);
+ int wpIdx = c.getColumnIndex(PreviewColumns.WALLPAPER_PREVIEW);
+ int cmpntIdIdx = c.getColumnIndex(PreviewColumns.COMPONENT_ID);
+ final Resources res = getResources();
+ Bitmap bitmap = Utils.loadBitmapBlob(c, wpIdx);
+ if (bitmap != null) {
+ mWallpaper.setImageBitmap(bitmap);
+ mWallpaperCard.setWallpaper(new BitmapDrawable(res, bitmap));
+ String pkgName = c.getString(pkgNameIdx);
+ Long cmpntId = (cmpntIdIdx >= 0) ? c.getLong(cmpntIdIdx) : DEFAULT_COMPONENT_ID;
+ if (!mPkgName.equals(pkgName) || (mPkgName.equals(pkgName)
+ && mBaseThemeSupportedComponents.contains(MODIFIES_LAUNCHER))) {
+ mSelectedComponentsMap.put(MODIFIES_LAUNCHER, pkgName);
+ mSelectedWallpaperComponentId = cmpntId;
+ setCardTitle(mWallpaperCard, pkgName, getString(R.string.wallpaper_label));
+ }
+ } else {
+ // Set the wallpaper to "None"
+ mWallpaperCard.setWallpaper(null);
+ setCardTitle(mWallpaperCard, WALLPAPER_NONE, getString(R.string.wallpaper_label));
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_LAUNCHER, WALLPAPER_NONE);
+ }
+
+ if (animate) {
+ animateContentChange(R.id.wallpaper_card, mWallpaperCard, overlay);
+ }
+ }
+
+ protected void loadLockScreen(Cursor c, boolean animate) {
+ mExternalLockscreenUri = null;
+ Drawable overlay = null;
+ if (animate) {
+ overlay = getOverlayDrawable(mLockScreenCard, true);
+ }
+ if (mLockScreenCard.isShowingEmptyView()) mLockScreenCard.setEmptyViewEnabled(false);
+
+ int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME);
+ int liveLockIndex = c.getColumnIndex(MODIFIES_LIVE_LOCK_SCREEN);
+ boolean isLiveLockScreen = liveLockIndex >= 0 && c.getInt(liveLockIndex) == 1;
+
+ int wpIdx = isLiveLockScreen
+ ? c.getColumnIndex(PreviewColumns.LIVE_LOCK_SCREEN_PREVIEW)
+ : c.getColumnIndex(PreviewColumns.LOCK_WALLPAPER_PREVIEW);
+ final Resources res = getResources();
+ Bitmap bitmap = Utils.loadBitmapBlob(c, wpIdx);
+ if (bitmap != null) {
+ mLockScreenCard.setWallpaper(new BitmapDrawable(res, bitmap));
+ String pkgName = c.getString(pkgNameIdx);
+ if (!mPkgName.equals(pkgName) || (mPkgName.equals(pkgName)
+ && (mBaseThemeSupportedComponents.contains(MODIFIES_LOCKSCREEN) ||
+ mBaseThemeSupportedComponents.contains(MODIFIES_LIVE_LOCK_SCREEN)))) {
+ if (isLiveLockScreen) {
+ mSelectedComponentsMap.put(MODIFIES_LIVE_LOCK_SCREEN, pkgName);
+ if (mCurrentTheme.containsKey(MODIFIES_LOCKSCREEN)) {
+ mSelectedComponentsMap.put(MODIFIES_LOCKSCREEN, LOCKSCREEN_NONE);
+ } else {
+ mSelectedComponentsMap.remove(MODIFIES_LOCKSCREEN);
+ }
+ setCardTitle(mLockScreenCard, pkgName,
+ getString(R.string.live_lock_screen_label));
+ } else {
+ mSelectedComponentsMap.put(MODIFIES_LOCKSCREEN, pkgName);
+ if (mCurrentTheme.containsKey(MODIFIES_LIVE_LOCK_SCREEN)) {
+ mSelectedComponentsMap.put(MODIFIES_LIVE_LOCK_SCREEN, LOCKSCREEN_NONE);
+ } else {
+ mSelectedComponentsMap.remove(MODIFIES_LIVE_LOCK_SCREEN);
+ }
+ setCardTitle(mLockScreenCard, pkgName, getString(R.string.lockscreen_label));
+ }
+ }
+ } else {
+ // Set the lockscreen wallpaper to "None"
+ mLockScreenCard.setWallpaper(null);
+ setCardTitle(mLockScreenCard, WALLPAPER_NONE, getString(R.string.lockscreen_label));
+ }
+
+ if (animate) {
+ animateContentChange(R.id.lockscreen_card, mLockScreenCard, overlay);
+ }
+ }
+
+ protected void loadStatusBar(Cursor c, boolean animate) {
+ int backgroundIdx = c.getColumnIndex(PreviewColumns.STATUSBAR_BACKGROUND);
+ int wifiIdx = c.getColumnIndex(PreviewColumns.STATUSBAR_WIFI_ICON);
+ int wifiMarginIdx = c.getColumnIndex(PreviewColumns.STATUSBAR_WIFI_COMBO_MARGIN_END);
+ int bluetoothIdx = c.getColumnIndex(PreviewColumns.STATUSBAR_BLUETOOTH_ICON);
+ int signalIdx = c.getColumnIndex(PreviewColumns.STATUSBAR_SIGNAL_ICON);
+ int batteryIdx = c.getColumnIndex(Utils.getBatteryIndex(mBatteryStyle));
+ int clockColorIdx = c.getColumnIndex(PreviewColumns.STATUSBAR_CLOCK_TEXT_COLOR);
+ int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME);
+
+ Bitmap background = Utils.loadBitmapBlob(c, backgroundIdx);
+ Bitmap bluetoothIcon = Utils.loadBitmapBlob(c, bluetoothIdx);
+ Bitmap wifiIcon = Utils.loadBitmapBlob(c, wifiIdx);
+ Bitmap signalIcon = Utils.loadBitmapBlob(c, signalIdx);
+ Bitmap batteryIcon = Utils.loadBitmapBlob(c, batteryIdx);
+ int wifiMargin = wifiMarginIdx != -1 ? c.getInt(wifiMarginIdx) : DEFAULT_WIFI_MARGIN;
+ int clockTextColor = clockColorIdx != -1 ? c.getInt(clockColorIdx) : DEFAULT_CLOCK_COLOR;
+
+ Drawable overlay = null;
+ if (animate) {
+ overlay = getOverlayDrawable(mStatusBar, false);
+ }
+ if (mStatusBarCard.isShowingEmptyView()) mStatusBarCard.setEmptyViewEnabled(false);
+
+ mStatusBar.setBackground(new BitmapDrawable(getActivity().getResources(), background));
+ mBluetooth.setImageBitmap(bluetoothIcon);
+ mWifi.setImageBitmap(wifiIcon);
+ mSignal.setImageBitmap(signalIcon);
+ mBattery.setImageBitmap(batteryIcon);
+ mClock.setTextColor(clockTextColor);
+
+ ViewGroup.MarginLayoutParams params =
+ (ViewGroup.MarginLayoutParams) mWifi.getLayoutParams();
+ params.setMarginEnd(wifiMargin);
+ mWifi.setLayoutParams(params);
+
+ if (mBatteryStyle == 4) {
+ mBattery.setVisibility(View.GONE);
+ } else {
+ mBattery.setVisibility(View.VISIBLE);
+ }
+ mStatusBar.post(new Runnable() {
+ @Override
+ public void run() {
+ mStatusBar.invalidate();
+ }
+ });
+ if (pkgNameIdx > -1) {
+ String pkgName = c.getString(pkgNameIdx);
+ if (!mPkgName.equals(pkgName) || (mPkgName.equals(pkgName)
+ && mBaseThemeSupportedComponents.contains(MODIFIES_STATUS_BAR))) {
+ mSelectedComponentsMap.put(MODIFIES_STATUS_BAR, pkgName);
+ setCardTitle(mStatusBarCard, pkgName,
+ getString(R.string.statusbar_label));
+ }
+ }
+ if (animate) {
+ animateContentChange(R.id.status_bar_container, mStatusBar, overlay);
+ }
+ }
+
+ protected void loadIcons(Cursor c, boolean animate) {
+ if (mIconCard.isShowingEmptyView()) {
+ mIconCard.setEmptyViewEnabled(false);
+ }
+ int[] iconIdx = new int[3];
+ iconIdx[0] = c.getColumnIndex(PreviewColumns.ICON_PREVIEW_1);
+ iconIdx[1] = c.getColumnIndex(PreviewColumns.ICON_PREVIEW_2);
+ iconIdx[2] = c.getColumnIndex(PreviewColumns.ICON_PREVIEW_3);
+ int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME);
+
+ // Set the icons. If the provider does not have an icon preview then
+ // fall back to the default icon set
+ IconPreviewHelper helper = new IconPreviewHelper(getActivity(), "");
+ ViewGroup iconContainer =
+ (ViewGroup) mIconCard.findViewById(R.id.icon_preview_container);
+ int numOfChildren = iconContainer.getChildCount();
+
+ List<ImageView> iconViews = new ArrayList<ImageView>(numOfChildren);
+ for(int i=0; i < numOfChildren; i++) {
+ final View view = iconContainer.getChildAt(i);
+ if (!(view instanceof ImageView)) continue;
+ iconViews.add((ImageView) view);
+ }
+
+ for(int i=0; i < iconViews.size() && i < iconIdx.length; i++) {
+ final ImageView v = iconViews.get(i);
+ Bitmap bitmap = Utils.loadBitmapBlob(c, iconIdx[i]);
+ Drawable oldIcon = v.getDrawable();
+ Drawable newIcon;
+ if (bitmap == null) {
+ ComponentName component = sIconComponents[i];
+ newIcon = helper.getDefaultIcon(component.getPackageName(),
+ component.getClassName());
+ } else {
+ newIcon = new BitmapDrawable(getResources(), bitmap);
+ }
+ if (animate) {
+ Drawable[] layers = new Drawable[2];
+ layers[0] = oldIcon instanceof IconTransitionDrawable ?
+ ((IconTransitionDrawable) oldIcon).getDrawable(1) : oldIcon;
+ layers[1] = newIcon;
+ final IconTransitionDrawable itd = new IconTransitionDrawable(layers);
+ v.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ itd.startTransition(ANIMATE_COMPONENT_CHANGE_DURATION);
+ v.setImageDrawable(itd);
+ }
+ }, ANIMATE_COMPONENT_ICON_DELAY * i);
+ } else {
+ v.setImageDrawable(newIcon);
+ }
+ }
+ if (pkgNameIdx > -1) {
+ String pkgName = c.getString(pkgNameIdx);
+ if (!mPkgName.equals(pkgName) || (mPkgName.equals(pkgName)
+ && mBaseThemeSupportedComponents.contains(MODIFIES_ICONS))) {
+ mSelectedComponentsMap.put(MODIFIES_ICONS, pkgName);
+ setCardTitle(mIconCard, pkgName,
+ getString(R.string.icon_label));
+ }
+ }
+ }
+
+ protected void loadNavBar(Cursor c, boolean animate) {
+ int backButtonIdx = c.getColumnIndex(PreviewColumns.NAVBAR_BACK_BUTTON);
+ int homeButtonIdx = c.getColumnIndex(PreviewColumns.NAVBAR_HOME_BUTTON);
+ int recentButtonIdx = c.getColumnIndex(PreviewColumns.NAVBAR_RECENT_BUTTON);
+ int backgroundIdx = c.getColumnIndex(PreviewColumns.NAVBAR_BACKGROUND);
+ if (backgroundIdx == -1) {
+ backgroundIdx = c.getColumnIndex(PreviewColumns.STATUSBAR_BACKGROUND);
+ }
+ int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME);
+
+ Bitmap background = Utils.loadBitmapBlob(c, backgroundIdx);
+ Bitmap backButton = Utils.loadBitmapBlob(c, backButtonIdx);
+ Bitmap homeButton = Utils.loadBitmapBlob(c, homeButtonIdx);
+ Bitmap recentButton = Utils.loadBitmapBlob(c, recentButtonIdx);
+
+ Drawable overlay = null;
+ if (animate) {
+ overlay = getOverlayDrawable(mNavBar, false);
+ }
+ if (mNavBarCard.isShowingEmptyView()) mNavBarCard.setEmptyViewEnabled(false);
+
+ mNavBar.setBackground(new BitmapDrawable(getActivity().getResources(), background));
+ mBackButton.setImageBitmap(backButton);
+ mHomeButton.setImageBitmap(homeButton);
+ mRecentButton.setImageBitmap(recentButton);
+
+ if (pkgNameIdx > -1) {
+ String pkgName = c.getString(pkgNameIdx);
+ if (!mPkgName.equals(pkgName) || (mPkgName.equals(pkgName)
+ && mBaseThemeSupportedComponents.contains(MODIFIES_NAVIGATION_BAR))) {
+ mSelectedComponentsMap.put(MODIFIES_NAVIGATION_BAR, pkgName);
+ setCardTitle(mNavBarCard, pkgName, getString(R.string.navbar_label));
+ }
+ }
+ if (animate) {
+ animateContentChange(R.id.navigation_bar_container, mNavBar, overlay);
+ }
+ }
+
+ protected void loadFont(Cursor c, boolean animate) {
+ Drawable overlay = null;
+ if (animate) {
+ overlay = getOverlayDrawable(mFontPreview, true);
+ }
+ if (mFontCard.isShowingEmptyView()) mFontCard.setEmptyViewEnabled(false);
+
+ int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME);
+ String pkgName = pkgNameIdx >= 0 ? c.getString(pkgNameIdx) : mPkgName;
+ TypefaceHelperCache cache = TypefaceHelperCache.getInstance();
+ ThemedTypefaceHelper helper = cache.getHelperForTheme(getActivity(), pkgName);
+ mTypefaceNormal = helper.getTypeface(Typeface.NORMAL);
+ mFontPreview.setTypeface(mTypefaceNormal);
+ if (!mPkgName.equals(pkgName) || (mPkgName.equals(pkgName)
+ && mBaseThemeSupportedComponents.contains(MODIFIES_FONTS))) {
+ mSelectedComponentsMap.put(MODIFIES_FONTS, pkgName);
+ setCardTitle(mFontCard, pkgName, getString(R.string.font_label));
+ }
+
+ if (animate) {
+ animateContentChange(R.id.font_preview_container, mFontPreview, overlay);
+ }
+ }
+
+ protected void loadStyle(Cursor c, boolean animate) {
+ Drawable overlay = null;
+ if (animate) {
+ overlay = getOverlayDrawable(mStylePreview, true);
+ }
+ if (mStyleCard.isShowingEmptyView()) {
+ mStyleCard.setEmptyViewEnabled(false);
+ }
+
+ int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME);
+ int styleIdx = c.getColumnIndex(PreviewColumns.STYLE_PREVIEW);
+ mStylePreview.setImageBitmap(Utils.loadBitmapBlob(c, styleIdx));
+ if (pkgNameIdx > -1) {
+ String pkgName = c.getString(pkgNameIdx);
+ if (!mPkgName.equals(pkgName) || (mPkgName.equals(pkgName)
+ && mBaseThemeSupportedComponents.contains(MODIFIES_OVERLAYS))) {
+ mSelectedComponentsMap.put(MODIFIES_OVERLAYS, pkgName);
+ setCardTitle(mStyleCard, pkgName,
+ getString(R.string.style_label));
+ }
+ }
+ if (animate) {
+ animateContentChange(R.id.style_card, mStylePreview, overlay);
+ }
+ }
+
+ protected void loadBootAnimation(Cursor c) {
+ if (mBootAnimationCard.isShowingEmptyView()) {
+ mBootAnimationCard.setEmptyViewEnabled(false);
+ }
+ int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME);
+ if (mBootAnimation != null) {
+ String pkgName;
+ if (pkgNameIdx > -1) {
+ pkgName = c.getString(pkgNameIdx);
+ if (!mPkgName.equals(pkgName) || (mPkgName.equals(pkgName)
+ && mBaseThemeSupportedComponents.contains(MODIFIES_BOOT_ANIM))) {
+ mSelectedComponentsMap.put(MODIFIES_BOOT_ANIM, pkgName);
+ setCardTitle(mBootAnimationCard, pkgName,
+ getString(R.string.boot_animation_label));
+ }
+ } else {
+ pkgName = mCurrentTheme.get(MODIFIES_BOOT_ANIM);
+ }
+ mBootAnimation.stop();
+ new AnimationLoader(getActivity(), pkgName, mBootAnimation).execute();
+ }
+ }
+
+ protected void loadAudible(int type, Cursor c, boolean animate) {
+ ComponentCardView audibleContainer = null;
+ ImageView playPause = null;
+ String component = null;
+ int parentResId = 0;
+ switch (type) {
+ case RingtoneManager.TYPE_RINGTONE:
+ audibleContainer = mRingtoneCard;
+ playPause = mRingtonePlayPause;
+ component = MODIFIES_RINGTONES;
+ parentResId = R.id.ringtone_preview_container;
+ break;
+ case RingtoneManager.TYPE_NOTIFICATION:
+ audibleContainer = mNotificationCard;
+ playPause = mNotificationPlayPause;
+ component = MODIFIES_NOTIFICATIONS;
+ parentResId = R.id.notification_preview_container;
+ break;
+ case RingtoneManager.TYPE_ALARM:
+ audibleContainer = mAlarmCard;
+ playPause = mAlarmPlayPause;
+ component = MODIFIES_ALARMS;
+ parentResId = R.id.alarm_preview_container;
+ break;
+ }
+ if (audibleContainer == null) return;
+
+ View content = audibleContainer.findViewById(R.id.content);
+ Drawable overlay = null;
+ if (animate) {
+ overlay = getOverlayDrawable(content, true);
+ }
+ if (audibleContainer.isShowingEmptyView()) {
+ audibleContainer.setEmptyViewEnabled(false);
+ }
+
+ int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME);
+ int titleIdx = c.getColumnIndex(ThemesColumns.TITLE);
+ if (playPause == null) {
+ playPause = (ImageView) audibleContainer.findViewById(R.id.play_pause);
+ }
+ TextView title = (TextView) audibleContainer.findViewById(R.id.audible_name);
+ MediaPlayer mp = mMediaPlayers.get(playPause);
+ if (mp == null) {
+ mp = new MediaPlayer();
+ }
+ String pkgName = c.getString(pkgNameIdx);
+ setCardTitle(audibleContainer, pkgName, getAudibleLabel(type));
+ AudibleLoadingThread thread = new AudibleLoadingThread(getActivity(), type, pkgName, mp);
+ title.setText(c.getString(titleIdx));
+ if (!mPkgName.equals(pkgName) || (mPkgName.equals(pkgName)
+ && mBaseThemeSupportedComponents.contains(component))) {
+ mSelectedComponentsMap.put(component, pkgName);
+ }
+
+ playPause.setVisibility(View.VISIBLE);
+ playPause.setTag(mp);
+ mMediaPlayers.put(playPause, mp);
+ playPause.setOnClickListener(mPlayPauseClickListener);
+ mp.setOnCompletionListener(mPlayCompletionListener);
+ if (animate) {
+ animateContentChange(parentResId, content, overlay);
+ }
+ thread.start();
+ }
+
+ protected Drawable getOverlayDrawable(View v, boolean requiresTransparency) {
+ if (!v.isDrawingCacheEnabled()) v.setDrawingCacheEnabled(true);
+ Bitmap cache = v.getDrawingCache(true).copy(
+ requiresTransparency ? Config.ARGB_8888 : Config.RGB_565, false);
+ Drawable d = cache != null ? new BitmapDrawable(getResources(), cache) : null;
+ v.destroyDrawingCache();
+
+ return d;
+ }
+
+ protected String getAudibleLabel(int type) {
+ switch (type) {
+ case RingtoneManager.TYPE_RINGTONE:
+ return getString(R.string.ringtone_label);
+ case RingtoneManager.TYPE_NOTIFICATION:
+ return getString(R.string.notification_label);
+ case RingtoneManager.TYPE_ALARM:
+ return getString(R.string.alarm_label);
+ }
+ return null;
+ }
+
+ protected void setCardTitle(ComponentCardView card, String pkgName, String title) {
+ TextView tv = (TextView) card.findViewById(R.id.label);
+ if (Utils.getDefaultThemePackageName(getActivity()).equals(pkgName)) {
+ tv.setText(getString(R.string.default_tag_text) + " " + title);
+ } else {
+ tv.setText(title);
+ }
+ }
+
+ protected void setAddComponentTitle(ComponentCardView card, String title) {
+ TextView tv = (TextView) card.findViewById(R.id.label);
+ tv.setText(getString(R.string.add_component_text) + " " + title);
+ }
+
+ public static ComponentName[] getIconComponents(Context context) {
+ if (sIconComponents == null || sIconComponents.length == 0) {
+ sIconComponents = new ComponentName[]{COMPONENT_DIALER, COMPONENT_MESSAGING,
+ COMPONENT_CAMERA, COMPONENT_BROWSER};
+
+ PackageManager pm = context.getPackageManager();
+
+ // if device does not have telephony replace dialer and mms
+ if (!pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+ sIconComponents[0] = COMPONENT_CALENDAR;
+ sIconComponents[1] = COMPONENT_GALERY;
+ } else {
+ // decide on which dialer icon to use
+ try {
+ if (pm.getPackageInfo(DIALER_NEXT_PACKAGE, 0) != null) {
+ sIconComponents[0] = COMPONENT_DIALERNEXT;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // default to COMPONENT_DIALER
+ }
+ }
+
+ if (!pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
+ sIconComponents[2] = COMPONENT_SETTINGS;
+ } else {
+ // decide on which camera icon to use
+ try {
+ if (pm.getPackageInfo(CAMERA_NEXT_PACKAGE, 0) != null) {
+ sIconComponents[2] = COMPONENT_CAMERANEXT;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // default to COMPONENT_CAMERA
+ }
+ }
+
+ }
+ return sIconComponents;
+ }
+
+ private void setupCardClickListeners(View parent) {
+ for (int i = 0; i < mCardIdsToComponentTypes.size(); i++) {
+ parent.findViewById(mCardIdsToComponentTypes.keyAt(i))
+ .setOnClickListener(mCardClickListener);
+ }
+ }
+
+ private View.OnClickListener mCardClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (isShowingConfirmCancelOverlay() || isShowingCustomizeResetLayout()) return;
+ if (mActiveCardId > 0) {
+ // need to fade the newly selected card in if another was currently selected.
+ ((ComponentCardView) v).animateCardFadeIn();
+ }
+ mActiveCardId = v.getId();
+ String component = mCardIdsToComponentTypes.get(mActiveCardId);
+ // Only pass on mSelectedWallpaperComponentId if dealing with mods_launcher
+ long selectedComponentId = (ThemesColumns.MODIFIES_LAUNCHER.equals(component)) ?
+ mSelectedWallpaperComponentId : DEFAULT_COMPONENT_ID;
+ String pkgName = mSelectedComponentsMap.get(component);
+ if (component.equals(MODIFIES_LOCKSCREEN) && TextUtils.isEmpty(pkgName)) {
+ String liveLockScreenPkg = mSelectedComponentsMap.get(MODIFIES_LIVE_LOCK_SCREEN);
+ if (liveLockScreenPkg != null) {
+ pkgName = liveLockScreenPkg;
+ }
+ }
+ getChooserActivity().showComponentSelector(component, pkgName, selectedComponentId, v);
+ fadeOutNonSelectedCards(mActiveCardId);
+ stopMediaPlayers();
+ }
+ };
+
+ private ConfirmCancelOverlay.OnOverlayDismissedListener mApplyCancelListener =
+ new ConfirmCancelOverlay.OnOverlayDismissedListener() {
+ @Override
+ public void onDismissed(boolean accepted) {
+ hideConfirmCancelOverlay(accepted);
+ }
+ };
+
+ private ConfirmCancelOverlay.OnOverlayDismissedListener mDeleteConfirmationListener =
+ new ConfirmCancelOverlay.OnOverlayDismissedListener() {
+ @Override
+ public void onDismissed(boolean accepted) {
+ if (accepted) uninstallTheme();
+ hideConfirmCancelOverlay();
+ }
+ };
+
+ private ConfirmCancelOverlay.OnOverlayDismissedListener mResetConfirmationListener =
+ new ConfirmCancelOverlay.OnOverlayDismissedListener() {
+ @Override
+ public void onDismissed(boolean accepted) {
+ if (accepted) resetTheme();
+ hideConfirmCancelOverlay();
+ }
+ };
+
+ private View.OnClickListener mCustomizeResetClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (v == mDismissButton) {
+ hideCustomizeResetLayout(CustomizeResetAction.Dismiss);
+ } else if (v == mResetButton) {
+ hideCustomizeResetLayout(CustomizeResetAction.Reset);
+ } else if (v == mCustomizeButton) {
+ hideCustomizeResetLayout(CustomizeResetAction.Customize);
+ }
+ }
+ };
+
+ protected void loadComponentFromPackage(String pkgName, String component, long componentId) {
+ Bundle args = new Bundle();
+ args.putString(ARG_PACKAGE_NAME, pkgName);
+ args.putLong(ARG_COMPONENT_ID, componentId);
+ int loaderId = LOADER_ID_INVALID;
+ if (MODIFIES_STATUS_BAR.equals(component)) {
+ loaderId = LOADER_ID_STATUS_BAR;
+ } else if (MODIFIES_FONTS.equals(component)) {
+ loaderId = LOADER_ID_FONT;
+ } else if (MODIFIES_ICONS.equals(component)) {
+ loaderId = LOADER_ID_ICONS;
+ } else if (MODIFIES_NAVIGATION_BAR.equals(component)) {
+ loaderId = LOADER_ID_NAVIGATION_BAR;
+ } else if (MODIFIES_LAUNCHER.equals(component)) {
+ if (pkgName != null) {
+ if (TextUtils.isEmpty(pkgName)) {
+ mWallpaperCard.setWallpaper(null);
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_LAUNCHER, WALLPAPER_NONE);
+ setCardTitle(mWallpaperCard, WALLPAPER_NONE,
+ getString(R.string.wallpaper_label));
+ getChooserActivity().showSaveApplyButton();
+ } else if (ComponentSelector.EXTERNAL_WALLPAPER.equals(pkgName)) {
+ // Check if we have READ_EXTERNAL_STORAGE permission and if not request it,
+ // otherwise let the user pick an image
+ if (getActivity().checkSelfPermission(
+ READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
+ mAfterPermissionGrantedRunnable = new Runnable() {
+ @Override
+ public void run() {
+ getChooserActivity().pickExternalWallpaper();
+ setCardTitle(mWallpaperCard, WALLPAPER_NONE,
+ getString(R.string.wallpaper_label));
+ }
+ };
+ requestPermissions(new String[]{READ_EXTERNAL_STORAGE},
+ PERMISSION_REQUEST);
+ } else {
+ getChooserActivity().pickExternalWallpaper();
+ setCardTitle(mWallpaperCard, WALLPAPER_NONE,
+ getString(R.string.wallpaper_label));
+ }
+ } else {
+ loaderId = LOADER_ID_WALLPAPER;
+ }
+ }
+ } else if (MODIFIES_LOCKSCREEN.equals(component)
+ || MODIFIES_LIVE_LOCK_SCREEN.equals(component)) {
+ if (pkgName != null && TextUtils.isEmpty(pkgName)) {
+ mLockScreenCard.setWallpaper(null);
+ mSelectedComponentsMap.put(ThemesColumns.MODIFIES_LOCKSCREEN, LOCKSCREEN_NONE);
+
+ if(mSelectedComponentsMap.containsKey(MODIFIES_LIVE_LOCK_SCREEN)) {
+ mSelectedComponentsMap.put(MODIFIES_LIVE_LOCK_SCREEN, LOCKSCREEN_NONE);
+ }
+ setCardTitle(mLockScreenCard, WALLPAPER_NONE,
+ getString(R.string.lockscreen_label));
+ if (mLockScreenCard.isShowingEmptyView()) {
+ mLockScreenCard.setEmptyViewEnabled(false);
+ }
+ getChooserActivity().showSaveApplyButton();
+ } else if (ComponentSelector.EXTERNAL_WALLPAPER.equals(pkgName)) {
+ // Check if we have READ_EXTERNAL_STORAGE permission and if not request it,
+ // otherwise let the user pick an image
+ if (getActivity().checkSelfPermission(
+ READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
+ mAfterPermissionGrantedRunnable = new Runnable() {
+ @Override
+ public void run() {
+ getChooserActivity().pickExternalLockscreen();
+ setCardTitle(mLockScreenCard, WALLPAPER_NONE,
+ getString(R.string.lockscreen_label));
+ }
+ };
+ requestPermissions(new String[] {READ_EXTERNAL_STORAGE},
+ PERMISSION_REQUEST);
+ } else {
+ getChooserActivity().pickExternalLockscreen();
+ setCardTitle(mLockScreenCard, WALLPAPER_NONE,
+ getString(R.string.lockscreen_label));
+ }
+ } else if (ComponentSelector.MOD_LOCK.equals(pkgName)) {
+ startLiveLockScreenSettings();
+ } else {
+ if (MODIFIES_LIVE_LOCK_SCREEN.equals(component)) {
+ loaderId = LOADER_ID_LIVE_LOCK_SCREEN;
+ } else {
+ loaderId = LOADER_ID_LOCKSCREEN;
+ }
+ }
+ } else if (MODIFIES_OVERLAYS.equals(component)) {
+ loaderId = LOADER_ID_STYLE;
+ } else if (MODIFIES_BOOT_ANIM.equals(component)) {
+ loaderId = LOADER_ID_BOOT_ANIMATION;
+ } else if (MODIFIES_RINGTONES.equals(component)) {
+ loaderId = LOADER_ID_RINGTONE;
+ } else if (MODIFIES_NOTIFICATIONS.equals(component)) {
+ loaderId = LOADER_ID_NOTIFICATION;
+ } else if (MODIFIES_ALARMS.equals(component)) {
+ loaderId = LOADER_ID_ALARM;
+ } else {
+ return;
+ }
+
+ if (loaderId != LOADER_ID_INVALID) {
+ getLoaderManager().restartLoader(loaderId, args, ThemeFragment.this);
+ }
+ }
+
+ private OnItemClickedListener mOnComponentItemClicked = new OnItemClickedListener() {
+ @Override
+ public void onItemClicked(String pkgName, long componentId, Bundle params) {
+ String component = mSelector.getComponentType();
+ if (MODIFIES_LOCKSCREEN.equals(component) && params != null) {
+ boolean isLiveLockView = params.getBoolean(
+ ComponentSelector.IS_LIVE_LOCK_SCREEN_VIEW,false);
+ if (isLiveLockView) {
+ //We got here because an live lock thubmnail view was clicked. We need to
+ //replace the component to load the proper data from the provider.
+ component = MODIFIES_LIVE_LOCK_SCREEN;
+ }
+ }
+ loadComponentFromPackage(pkgName, component, componentId);
+ }
+ };
+
+ private void fadeOutNonSelectedCards(int selectedCardId) {
+ for (int i = 0; i < mCardIdsToComponentTypes.size(); i++) {
+ if (mCardIdsToComponentTypes.keyAt(i) != selectedCardId) {
+ ComponentCardView card = (ComponentCardView) getView().findViewById(
+ mCardIdsToComponentTypes.keyAt(i));
+ if (card != null) card.animateCardFadeOut();
+ }
+ }
+ }
+
+ protected void animateContentChange(int parentId, View viewToAnimate, Drawable overlay) {
+ ((ComponentCardView) getView().findViewById(parentId))
+ .animateContentChange(viewToAnimate, overlay, ANIMATE_COMPONENT_CHANGE_DURATION);
+ }
+
+ private Runnable mApplyThemeRunnable = new Runnable() {
+ @Override
+ public void run() {
+ final Context context = getActivity();
+ if (context != null) {
+ // Post this on mHandler so the client is added and removed from the same
+ // thread
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final Map<String, String> componentsToApply = getComponentsToApply();
+ if (componentsToApply != null && componentsToApply.size() > 0) {
+ final Map<String, String> fullMap
+ = fillMissingComponentsWithDefault(componentsToApply);
+ ThemeManager tm = getThemeManager();
+ if (tm != null) {
+ try {
+ tm.addClient(ThemeFragment.this);
+ } catch (IllegalArgumentException e) {
+ /* ignore since this means we already have a listener added */
+ }
+ ThemeChangeRequest request =
+ getThemeChangeRequestForComponents(fullMap);
+ boolean value = request.getReqeustType().
+ equals(RequestType.USER_REQUEST_MIXNMATCH);
+
+ tm.requestThemeChange(request, !value);
+ }
+ mApplyThemeOnPopulated = false;
+ } else {
+ onFinish(true);
+ }
+ }
+ });
+ }
+ }
+ };
+
+ protected Map<String, String> fillMissingComponentsWithDefault(
+ Map<String, String> originalMap) {
+ HashMap newMap = new HashMap<String, String>();
+ newMap.putAll(originalMap);
+ Map<String, String> defaultMap = getEmptyComponentsMap();
+ for(Map.Entry<String, String> entry : defaultMap.entrySet()) {
+ String component = entry.getKey();
+ String defaultPkg = entry.getValue();
+ if (!newMap.containsKey(component)) {
+ newMap.put(component, defaultPkg);
+ }
+ }
+ return newMap;
+ }
+
+ protected Map<String, String> getEmptyComponentsMap() {
+ List<String> componentsList = ThemeUtils.getAllComponents();
+ Map<String, String> defaultMap = new HashMap<>(componentsList.size());
+ for (String component : componentsList) {
+ defaultMap.put(component, "");
+ }
+ return defaultMap;
+ }
+
+ /**
+ * This is the method that will be called when applying a theme and the idea is to override
+ * it in MyThemeFragment and pass in a different RequestType, once we have a type that indicates
+ * the user is mixing and matching instead of applying an entire theme.
+ * @param componentMap
+ * @return
+ */
+ protected ThemeChangeRequest getThemeChangeRequestForComponents(
+ Map<String, String> componentMap) {
+ return getThemeChangeRequestForComponents(componentMap, RequestType.USER_REQUEST);
+ }
+
+ protected ThemeChangeRequest getThemeChangeRequestForComponents(
+ Map<String, String> componentMap, RequestType requestType) {
+ ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder();
+ for (String component : componentMap.keySet()) {
+ builder.setComponent(component, componentMap.get(component));
+ }
+ builder.setRequestType(requestType);
+ if (mThemeVersion >= 3) {
+ builder.setWallpaperId(mSelectedWallpaperComponentId != null
+ ? mSelectedWallpaperComponentId
+ : DEFAULT_COMPONENT_ID);
+ }
+ return builder.build();
+ }
+
+ protected Map<String, String> getComponentsToApply() {
+ return mSelectedComponentsMap;
+ }
+
+ private Runnable mApplyExternalWallpaperRunnable = new Runnable() {
+ @Override
+ public void run() {
+ // If an external image was selected for the wallpaper, we need to
+ // set that manually.
+ if (mExternalWallpaperUri != null) {
+ WallpaperManager wm =
+ WallpaperManager.getInstance(getActivity());
+ final Context context = getActivity();
+ final Resources res = context.getResources();
+ final Point size = new Point(wm.getDesiredMinimumWidth(),
+ wm.getDesiredMinimumHeight());
+ Bitmap bmp = WallpaperUtils.createPreview(size, context, mExternalWallpaperUri,
+ null, res, 0, 0, false);
+ try {
+ wm.setBitmap(bmp);
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to set external wallpaper", e);
+ }
+ }
+ }
+ };
+
+ private Runnable mApplyExternalLockscreenRunnable = new Runnable() {
+ @Override
+ public void run() {
+ // If an external image was selected for the wallpaper, we need to
+ // set that manually.
+ if (mExternalLockscreenUri != null) {
+ WallpaperManager wm =
+ WallpaperManager.getInstance(getActivity());
+ final Context context = getActivity();
+ final Resources res = context.getResources();
+ final Point size = new Point();
+ ((Activity) context).getWindowManager().getDefaultDisplay().getRealSize(size);
+ Bitmap bmp = WallpaperUtils.createPreview(size, context, mExternalLockscreenUri,
+ null, res, 0, 0, false);
+ try {
+ wm.setKeyguardBitmap(bmp);
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to set external lockscreen wallpaper", e);
+ }
+ }
+ }
+ };
+
+ class RestoreLockScreenCardRunnable implements Runnable {
+
+ final private boolean mWasShowingNone;
+ private String mCurrentLockScreenPkgName;
+
+ public RestoreLockScreenCardRunnable(boolean w, String pkgName) {
+ mWasShowingNone = w;
+ mCurrentLockScreenPkgName = pkgName;
+ }
+
+ @Override
+ public void run() {
+ if (!TextUtils.isEmpty(mCurrentLockScreenPkgName)) {
+ loadComponentFromPackage(mCurrentLockScreenPkgName,
+ MODIFIES_LOCKSCREEN, 0);
+ } else if (mWasShowingNone) {
+ mLockScreenCard.setWallpaper(null);
+ TextView none = (TextView) mLockScreenCard.findViewById(
+ R.id.none);
+ if (none != null) {
+ none.setVisibility(View.VISIBLE);
+ }
+ mLockScreenCard.setEmptyViewEnabled(false);
+ } else {
+ mLockScreenCard.clearWallpaper();
+ TextView none = (TextView) mLockScreenCard.findViewById(
+ R.id.none);
+ if (none != null) {
+ none.setVisibility(View.GONE);
+ }
+ mLockScreenCard.setEmptyViewEnabled(true);
+ setAddComponentTitle(mLockScreenCard, getString(R.string.lockscreen_label));
+ }
+ }
+ }
+
+ protected void applyTheme() {
+ if (mExternalWallpaperUri == null && mExternalLockscreenUri == null &&
+ (mSelectedComponentsMap == null || mSelectedComponentsMap.size() <= 0)) {
+ return;
+ }
+ final Map<String,String> componentsToApply = getComponentsToApply();
+ boolean isLLSEnabled = CMSettings.Secure.getInt(getActivity().getContentResolver(),
+ LIVE_LOCK_SCREEN_ENABLED, 0) == 1;
+ if (!TextUtils.isEmpty(componentsToApply.get(MODIFIES_LIVE_LOCK_SCREEN)) && !isLLSEnabled) {
+ AlertDialog d = new AlertDialog.Builder(getActivity(),
+ android.R.style.Theme_Material_Dialog)
+ .setTitle(R.string.enable_live_lock_screen_dialog_title)
+ .setMessage(R.string.enable_live_lock_screen_dialog_message)
+ .setPositiveButton(R.string.enable_live_lock_screen_dialog_positive_btn_text,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ CMSettings.Secure.putInt(getActivity().getContentResolver(),
+ LIVE_LOCK_SCREEN_ENABLED, 1);
+ getChooserActivity().themeChangeStart();
+ animateProgressIn(mApplyThemeRunnable);
+ }
+ })
+ .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mSelectedComponentsMap.remove(MODIFIES_LIVE_LOCK_SCREEN);
+ boolean wasNone = false;
+ if (TextUtils.equals("", mSelectedComponentsMap.get(
+ MODIFIES_LOCKSCREEN))) {
+ if (!TextUtils.isEmpty(mCurrentTheme.get(MODIFIES_LOCKSCREEN))) {
+ //The map entry was set to empty string because there's a
+ //lockscreen currently applied and setting this entry to empty
+ //would instruct the ThemeManager to clear the currently applied
+ //lockscreen, but the user decided not to enable LLS so we need
+ //to abort this change
+ mSelectedComponentsMap.put(MODIFIES_LOCKSCREEN,
+ mCurrentTheme.get(MODIFIES_LOCKSCREEN));
+ } else {
+ wasNone = true;
+ }
+ }
+ //Restore the lockscreen card to its previous state
+ mHandler.post(new RestoreLockScreenCardRunnable(wasNone,
+ mCurrentTheme.get(MODIFIES_LOCKSCREEN)));
+
+ //Did the user make more changes?
+ boolean modLLS = componentsToApply.containsKey(
+ MODIFIES_LIVE_LOCK_SCREEN);
+ boolean modLockscreen = componentsToApply.containsKey(
+ MODIFIES_LOCKSCREEN);
+ if (modLLS && ((modLockscreen && componentsToApply.size() > 2) ||
+ (!modLockscreen && componentsToApply.size() > 1))) {
+ getChooserActivity().themeChangeStart();
+ animateProgressIn(mApplyThemeRunnable);
+ }
+ }
+ })
+ .setCancelable(false)
+ .create();
+ d.setCanceledOnTouchOutside(false);
+ d.show();
+ } else {
+ getChooserActivity().themeChangeStart();
+ animateProgressIn(mApplyThemeRunnable);
+ }
+ }
+
+ /**
+ * Use when applyTheme() might be too early. ie mSelectedComponentsMap is not pop. yet
+ * @param pkgName Only used in MyThemeFragment to apply components on top of current theme
+ * @param components Optional list of components to apply.
+ */
+ protected void applyThemeWhenPopulated(String pkgName, List<String> components) {
+ mApplyThemeOnPopulated = true;
+ }
+
+ private void animateProgressIn(Runnable endAction) {
+ mProgress.setVisibility(View.VISIBLE);
+ mProgress.setProgress(0);
+ float pivotX = mTitleLayout.getWidth() -
+ getResources().getDimensionPixelSize(R.dimen.apply_progress_padding);
+ ScaleAnimation scaleAnim = new ScaleAnimation(0f, 1f, 1f, 1f,
+ pivotX, 0f);
+ scaleAnim.setDuration(ANIMATE_PROGRESS_IN_DURATION);
+
+ mTitleLayout.animate()
+ .translationXBy(-(pivotX / 3))
+ .alpha(0f)
+ .setDuration(ANIMATE_TITLE_OUT_DURATION)
+ .setInterpolator(new AccelerateInterpolator())
+ .withEndAction(endAction).start();
+ mProgress.startAnimation(scaleAnim);
+ }
+
+ private void animateProgressOut() {
+ mProgress.setVisibility(View.VISIBLE);
+ float pivotX = mTitleLayout.getWidth() -
+ getResources().getDimensionPixelSize(R.dimen.apply_progress_padding);
+ ScaleAnimation scaleAnim = new ScaleAnimation(1f, 0f, 1f, 1f,
+ pivotX, 0f);
+ scaleAnim.setDuration(ANIMATE_PROGRESS_OUT_DURATION);
+ scaleAnim.setFillAfter(false);
+ scaleAnim.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mProgress.setVisibility(View.GONE);
+ if (mThemeResetting) {
+ mThemeResetting = false;
+ mThemeTagLayout.setCustomizedTagEnabled(false);
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+ });
+
+ mTitleLayout.animate()
+ .translationXBy((pivotX / 3))
+ .alpha(1f)
+ .setDuration(ANIMATE_TITLE_IN_DURATION)
+ .setInterpolator(new AccelerateInterpolator())
+ .start();
+ mProgress.startAnimation(scaleAnim);
+ if (mThemeResetting) mReset.setVisibility(View.GONE);
+ }
+
+ private void animateContentIn() {
+ if (mSkipLoadingAnim) {
+ return;
+ }
+ AnimatorSet set = new AnimatorSet();
+ set.setDuration(ANIMATE_TITLE_IN_DURATION);
+ set.play(ObjectAnimator.ofFloat(mLoadingView, "alpha", 1f, 0f))
+ .with(ObjectAnimator.ofFloat(mTitleLayout, "alpha", 0f, 1f));
+ set.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mLoadingView.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+ set.start();
+ }
+
+ private void disableActionButtons() {
+ mCustomize.setEnabled(false);
+ mDelete.setEnabled(false);
+ mReset.setEnabled(false);
+ }
+
+ private void enableActionButtons() {
+ mCustomize.setEnabled(true);
+ mDelete.setEnabled(true);
+ mReset.setEnabled(true);
+ }
+
+ public boolean isShowingConfirmCancelOverlay() {
+ return mConfirmCancelOverlay.getVisibility() == View.VISIBLE;
+ }
+
+ public void showApplyThemeOverlay() {
+ if (mConfirmCancelOverlay.getVisibility() == View.VISIBLE) return;
+ mConfirmCancelOverlay.setTitle(R.string.apply_theme_overlay_title);
+ mConfirmCancelOverlay.setBackgroundColor(getActivity().getResources()
+ .getColor(R.color.apply_overlay_background));
+ mConfirmCancelOverlay.setOnOverlayDismissedListener(mApplyCancelListener);
+ getChooserActivity().lockPager();
+ ViewPropertyAnimator anim = mConfirmCancelOverlay.animate();
+ mConfirmCancelOverlay.setVisibility(View.VISIBLE);
+ mConfirmCancelOverlay.setAlpha(0f);
+ anim.setListener(null);
+ anim.setDuration(ANIMATE_APPLY_LAYOUT_DURATION);
+ anim.alpha(1f).start();
+
+ if (mIsLegacyTheme) {
+ // Display cm11 theme warning message
+ TextView tv = (TextView) mConfirmCancelOverlay.findViewById(R.id.warning_message);
+ tv.setVisibility(View.VISIBLE);
+ tv.setText(String.format(getString(R.string.legacy_theme_warning), mTitle.getText()));
+ } else if (Utils.hasPerAppThemesApplied(getActivity())) {
+ // Display per app theme changes will be removed warning
+ TextView tv = (TextView) mConfirmCancelOverlay.findViewById(R.id.warning_message);
+ tv.setVisibility(View.VISIBLE);
+ tv.setText(String.format(getString(R.string.per_app_theme_removal_warning),
+ mTitle.getText()));
+ }
+
+ disableActionButtons();
+ mClickableView.setSoundEffectsEnabled(false);
+ }
+
+ public void showDeleteThemeOverlay() {
+ if (mConfirmCancelOverlay.getVisibility() == View.VISIBLE) return;
+ mConfirmCancelOverlay.setTitle(R.string.delete_theme_overlay_title);
+ mConfirmCancelOverlay.setBackgroundColor(getActivity().getResources()
+ .getColor(R.color.delete_overlay_background));
+ mConfirmCancelOverlay.setOnOverlayDismissedListener(mDeleteConfirmationListener);
+ getChooserActivity().lockPager();
+ ViewPropertyAnimator anim = mConfirmCancelOverlay.animate();
+ mConfirmCancelOverlay.setVisibility(View.VISIBLE);
+ mConfirmCancelOverlay.setAlpha(0f);
+ anim.setListener(null);
+ anim.setDuration(ANIMATE_APPLY_LAYOUT_DURATION);
+ anim.alpha(1f).start();
+
+ disableActionButtons();
+ mClickableView.setSoundEffectsEnabled(false);
+ }
+
+ public void showResetThemeOverlay() {
+ if (mConfirmCancelOverlay.getVisibility() == View.VISIBLE) return;
+ mConfirmCancelOverlay.setTitle(R.string.reset_theme_overlay_title);
+ mConfirmCancelOverlay.setBackgroundColor(getActivity().getResources()
+ .getColor(R.color.apply_overlay_background));
+ mConfirmCancelOverlay.setOnOverlayDismissedListener(mResetConfirmationListener);
+ getChooserActivity().lockPager();
+ ViewPropertyAnimator anim = mConfirmCancelOverlay.animate();
+ mConfirmCancelOverlay.setVisibility(View.VISIBLE);
+ mConfirmCancelOverlay.setAlpha(0f);
+ anim.setListener(null);
+ anim.setDuration(ANIMATE_APPLY_LAYOUT_DURATION);
+ anim.alpha(1f).start();
+
+ disableActionButtons();
+ mClickableView.setSoundEffectsEnabled(false);
+ }
+
+ public void hideConfirmCancelOverlay() {
+ hideConfirmCancelOverlay(false);
+ }
+
+ /**
+ * Hides the apply theme layout overlay and can apply the selected theme
+ * when the animation is finished.
+ * @param applyThemeWhenFinished If true, the current theme will be applied.
+ */
+ private void hideConfirmCancelOverlay(final boolean applyThemeWhenFinished) {
+ getChooserActivity().unlockPager();
+ ViewPropertyAnimator anim = mConfirmCancelOverlay.animate();
+ mConfirmCancelOverlay.setVisibility(View.VISIBLE);
+ anim.setDuration(ANIMATE_APPLY_LAYOUT_DURATION);
+ anim.alpha(0f).start();
+ anim.setListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mConfirmCancelOverlay.setVisibility(View.GONE);
+ if (applyThemeWhenFinished) applyTheme();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+
+ enableActionButtons();
+ mClickableView.setSoundEffectsEnabled(true);
+ }
+
+ public boolean isShowingCustomizeResetLayout() {
+ return mCustomizeResetLayout.getVisibility() == View.VISIBLE;
+ }
+
+ public void showCustomizeResetLayout() {
+ if (mCustomizeResetLayout.getVisibility() == View.VISIBLE) return;
+ if (!mThemeTagLayout.isCustomizedTagEnabled()) {
+ mResetButton.setEnabled(false);
+ } else {
+ mResetButton.setEnabled(true);
+ }
+ getChooserActivity().lockPager();
+ ViewPropertyAnimator anim = mCustomizeResetLayout.animate();
+ mCustomizeResetLayout.setVisibility(View.VISIBLE);
+ mCustomizeResetLayout.setAlpha(0f);
+ anim.setListener(null);
+ anim.setDuration(ANIMATE_APPLY_LAYOUT_DURATION);
+ anim.alpha(1f).start();
+
+ disableActionButtons();
+ mClickableView.setSoundEffectsEnabled(false);
+ }
+
+ public void hideCustomizeResetLayout() {
+ hideCustomizeResetLayout(CustomizeResetAction.Dismiss);
+ }
+
+ private void hideCustomizeResetLayout(final CustomizeResetAction action) {
+ getChooserActivity().unlockPager();
+ ViewPropertyAnimator anim = mCustomizeResetLayout.animate();
+ mCustomizeResetLayout.setVisibility(View.VISIBLE);
+ anim.setDuration(ANIMATE_APPLY_LAYOUT_DURATION);
+ anim.alpha(0f).start();
+ anim.setListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCustomizeResetLayout.setVisibility(View.GONE);
+ switch (action) {
+ case Customize:
+ getChooserActivity().expand();
+ break;
+ case Reset:
+ resetTheme();
+ break;
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+
+ enableActionButtons();
+ mClickableView.setSoundEffectsEnabled(true);
+ }
+
+ public void showThemeTagLayout() {
+ mThemeTagLayout.setVisibility(View.VISIBLE);
+ mThemeTagLayout.animate().alpha(1f).setStartDelay(ANIMATE_START_DELAY).start();
+ }
+
+ public void hideThemeTagLayout() {
+ mThemeTagLayout.setAlpha(0f);
+ mThemeTagLayout.setVisibility(View.GONE);
+ }
+
+ public void hideProcessingOverlay() {
+ mProcessingThemeLayout.animate().alpha(0).withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mProcessingThemeLayout.setVisibility(View.GONE);
+ }
+ }).setDuration(ANIMATE_APPLY_LAYOUT_DURATION).start();
+ mCustomize.setVisibility(View.VISIBLE);
+ mCustomize.setAlpha(0f);
+ mCustomize.animate().alpha(1f).setDuration(ANIMATE_APPLY_LAYOUT_DURATION).start();
+ if (mDelete.getVisibility() != View.GONE) {
+ mDelete.setVisibility(View.VISIBLE);
+ mDelete.setAlpha(0f);
+ mDelete.animate().alpha(1f).setDuration(ANIMATE_APPLY_LAYOUT_DURATION).start();
+ }
+
+ enableActionButtons();
+ mClickableView.setSoundEffectsEnabled(true);
+ }
+
+ public void fadeInCards() {
+ for (int i = 0; i < mCardIdsToComponentTypes.size(); i++) {
+ final int key = mCardIdsToComponentTypes.keyAt(i);
+ if (key != mActiveCardId) {
+ ComponentCardView card = (ComponentCardView) getView().findViewById(key);
+ if (card != null) card.animateCardFadeIn();
+ }
+ }
+ mActiveCardId = -1;
+ }
+
+ public boolean componentsChanged() {
+ // If an external wallpaper/ls are set then something changed!
+ if (mExternalWallpaperUri != null || mExternalLockscreenUri != null) return true;
+
+ for (String key : mSelectedComponentsMap.keySet()) {
+ if (!mPkgName.equals(mSelectedComponentsMap.get(key))) {
+ return true;
+ }
+ if (ThemesColumns.MODIFIES_LAUNCHER.equals(key) &&
+ mCurrentWallpaperComponentId.value != mSelectedWallpaperComponentId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected boolean isThemeCustomized() {
+ final String themePkgName = getThemePackageName();
+ for (String key : mSelectedComponentsMap.keySet()) {
+ final String selectedPkgName = mSelectedComponentsMap.get(key);
+ if (!themePkgName.equals(selectedPkgName)) {
+ return true;
+ }
+ if (mBaseThemeSupportedComponents.size() > 0 &&
+ !mBaseThemeSupportedComponents.contains(key)) {
+ return true;
+ }
+ }
+ // finally check if we're missing anything from mBaseThemeSupportedComponents
+ for (String component : mBaseThemeSupportedComponents) {
+ if (!mSelectedComponentsMap.containsKey(component)) return true;
+ }
+ return false;
+ }
+
+ public void clearChanges() {
+ mSelectedComponentsMap.clear();
+ mExternalWallpaperUri = null;
+ mExternalLockscreenUri = null;
+ View none = mLockScreenCard.findViewById(R.id.none);
+ if (none != null && none.getVisibility() == View.VISIBLE) {
+ none.setVisibility(View.GONE);
+ }
+ TextView tv = (TextView) mLockScreenCard.findViewById(R.id.label);
+ if (tv != null) {
+ tv.setAlpha(1f);
+ tv.setBackgroundResource(R.drawable.wallpaper_label_bg);
+ }
+ getLoaderManager().restartLoader(LOADER_ID_ALL, null, ThemeFragment.this);
+ }
+
+ public String getThemePackageName() {
+ if (mPkgName == null) {
+ // check if the package name is defined in the arguments bundle
+ Bundle bundle = getArguments();
+ if (bundle != null) {
+ mPkgName = bundle.getString(ARG_PACKAGE_NAME);
+ }
+ }
+ return mPkgName;
+ }
+
+ private void uninstallTheme() {
+ getChooserActivity().uninstallTheme(mPkgName);
+ }
+
+ public void setCurrentTheme(Map<String, String> currentTheme,
+ MutableLong currentWallpaperComponentId) {
+ mCurrentTheme = currentTheme;
+ mCurrentWallpaperComponentId = currentWallpaperComponentId;
+ }
+
+ /**
+ * Slides the scrollview content up and adds a space view at the bottom
+ * of mAdditionalCards so all content can be visible above the selector.
+ *
+ * We are using a ValueAnimator here to scroll the content rather than calling
+ * mScrollView.smoothScrollBy() since the speed of that animation cannot be customized.
+ * @param yDelta
+ * @param selectorHeight
+ */
+ public void slideContentIntoView(final int yDelta, int selectorHeight) {
+ Space space = (Space) mAdditionalCards.findViewById(ADDITIONAL_CONTENT_SPACE_ID);
+ if (space == null) {
+ // No space view yet so lets create it one
+ space = new Space(getActivity());
+ space.setId(ADDITIONAL_CONTENT_SPACE_ID);
+ mAdditionalCards.addView(space,
+ new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
+ selectorHeight));
+ } else {
+ // Space view already exists so just update the LayoutParams
+ LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) space.getLayoutParams();
+ params.height = selectorHeight;
+ space.setLayoutParams(params);
+ }
+ final int startY = mScrollView.getScrollY();
+ final ValueAnimator scrollAnimator =
+ ValueAnimator.ofInt(startY, startY + yDelta);
+ scrollAnimator.setDuration(SLIDE_CONTENT_ANIM_DURATION);
+ scrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ int value = (Integer) animation.getAnimatedValue();
+ mScrollView.scrollTo(0, value);
+ }
+ });
+ scrollAnimator.start();
+ }
+
+ public Map<String, String> getSelectedComponentsMap() {
+ return mSelectedComponentsMap;
+ }
+
+ /**
+ * Slides the scrollview content down and removes a space view at the bottom
+ * of mAdditionalCards.
+ *
+ * We are using a ValueAnimator here to scroll the content rather than calling
+ * mScrollView.smoothScrollBy() since the speed of that animation cannot be customized.
+ * @param yDelta
+ */
+ public void slideContentBack(int yDelta) {
+ final int startY = mScrollView.getScrollY();
+ final ValueAnimator scrollAnimator =
+ ValueAnimator.ofInt(startY, startY + yDelta);
+ scrollAnimator.setDuration(SLIDE_CONTENT_ANIM_DURATION);
+ scrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ int value = (Integer) animation.getAnimatedValue();
+ mScrollView.scrollTo(0, value);
+ }
+ });
+ scrollAnimator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ View space = mAdditionalCards.findViewById(ADDITIONAL_CONTENT_SPACE_ID);
+ if (space != null) mAdditionalCards.removeView(space);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+ scrollAnimator.start();
+ }
+
+ public void showLockScreenCard() {
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ final int scrollToY = mStatusBarCard.getMeasuredHeight()
+ + mFontCard.getMeasuredHeight() + mIconCard.getMeasuredHeight()
+ + mNavBarCard.getMeasuredHeight() + mWallpaperCard.getMeasuredHeight() / 2;
+ final ValueAnimator scrollAnimator = ValueAnimator.ofInt(0, scrollToY);
+ scrollAnimator.setDuration(LOCK_SCREEN_CARD_SCROLL_ANIMATION_DURATION);
+ scrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ int value = (Integer) animation.getAnimatedValue();
+ mScrollView.scrollTo(0, value);
+ }
+ });
+ scrollAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mCardClickListener.onClick(mLockScreenCard);
+ }
+ });
+ scrollAnimator.start();
+ }
+ }, SHOW_LOCK_SCREEN_CARD_DELAY);
+ }
+
+ protected void startLiveLockScreenSettings() {
+ Intent intent = new Intent(cyanogenmod.content.Intent.ACTION_OPEN_LIVE_LOCKSCREEN_SETTINGS);
+ try {
+ startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ // TODO: inform user that this action failed (Toast?)
+ }
+ }
+
+ class AnimationLoader extends AsyncTask<Void, Void, Boolean> {
+ Context mContext;
+ String mPkgName;
+ BootAniImageView mBootAnim;
+
+ public AnimationLoader(Context context, String pkgName, BootAniImageView bootAnim) {
+ mContext = context;
+ mPkgName = pkgName;
+ mBootAnim = bootAnim;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ if (mContext == null) {
+ return Boolean.FALSE;
+ }
+ ZipFile zip = null;
+ if (ThemeConfig.SYSTEM_DEFAULT.equals(mPkgName)) {
+ try {
+ zip = new ZipFile(new File(BootAnimationHelper.SYSTEM_BOOT_ANI_PATH));
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to load boot animation", e);
+ return Boolean.FALSE;
+ }
+ } else {
+ // check if the bootanimation is cached
+ File f = new File(mContext.getCacheDir(),
+ mPkgName + BootAnimationHelper.CACHED_SUFFIX);
+ if (!f.exists()) {
+ // go easy on cache storage and clear out any previous boot animations
+ BootAnimationHelper.clearBootAnimationCache(mContext);
+ try {
+ Context themeContext = mContext.createPackageContext(mPkgName, 0);
+ AssetManager am = themeContext.getAssets();
+ InputStream is = am.open("bootanimation/bootanimation.zip");
+ FileUtils.copyToFile(is, f);
+ is.close();
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to load boot animation", e);
+ return Boolean.FALSE;
+ }
+ }
+ try {
+ zip = new ZipFile(f);
+ } catch (IOException e) {
+ Log.w(TAG, "Unable to load boot animation", e);
+ return Boolean.FALSE;
+ }
+ }
+ if (zip != null) {
+ mBootAnim.setBootAnimation(zip);
+ } else {
+ return Boolean.FALSE;
+ }
+ return Boolean.TRUE;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean isSuccess) {
+ super.onPostExecute(isSuccess);
+ if (isSuccess) {
+ mBootAnim.start();
+ }
+ }
+ }
+
+ class AudibleLoadingThread extends Thread {
+ private Context mContext;
+ private int mType;
+ private String mPkgName;
+ private MediaPlayer mPlayer;
+
+ public AudibleLoadingThread(Context context, int type, String pkgName, MediaPlayer mp) {
+ super();
+ mContext = context;
+ mType = type;
+ mPkgName = pkgName;
+ mPlayer = mp;
+ }
+
+ @Override
+ public void run() {
+ try {
+ AudioUtils.loadThemeAudible(mContext, mType, mPkgName, mPlayer);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Unable to load sound for " + mPkgName, e);
+ }
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/chooser/WallpaperCardView.java b/src/org/cyanogenmod/theme/chooser/WallpaperCardView.java
new file mode 100644
index 0000000..a3ed8ea
--- /dev/null
+++ b/src/org/cyanogenmod/theme/chooser/WallpaperCardView.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewOverlay;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class WallpaperCardView extends ComponentCardView {
+ protected ImageView mImage;
+ protected TextView mLabel;
+
+ public WallpaperCardView(Context context) {
+ this(context, null);
+ }
+
+ public WallpaperCardView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public WallpaperCardView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WallpaperCardView);
+ String labelText = a.getString(R.styleable.WallpaperCardView_labelText);
+ a.recycle();
+
+ setOrientation(VERTICAL);
+
+ setBackgroundResource(R.drawable.card_bg);
+
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ FrameLayout frameLayout =
+ (FrameLayout) inflater.inflate(R.layout.wallpaper_card, this, false);
+ addView(frameLayout);
+ mLabel = (TextView) frameLayout.findViewById(R.id.label);
+ mImage = (ImageView) frameLayout.findViewById(R.id.image);
+
+ mLabel.setText(labelText);
+ }
+
+ public void setWallpaper(Drawable drawable) {
+ mImage.setImageDrawable(drawable);
+ View none = findViewById(R.id.none);
+ if (drawable == null) {
+ setBackgroundResource(R.drawable.card_wallpapertoggled_bg);
+ if (none != null) {
+ none.setVisibility(View.VISIBLE);
+ }
+ if (mLabel != null) {
+ mLabel.setBackgroundResource(0);
+ }
+ } else {
+ setBackgroundResource(0);
+
+ if (none != null) {
+ none.setVisibility(View.GONE);
+ }
+ if (mLabel != null) {
+ mLabel.setBackgroundResource(R.drawable.wallpaper_label_bg);
+ }
+ }
+ }
+
+ public void clearWallpaper() {
+ mImage.setImageDrawable(null);
+ setBackgroundResource(R.drawable.card_bg);
+ }
+
+ public Drawable getWallpaperDrawable() {
+ return mImage.getDrawable();
+ }
+
+ @Override
+ public void expand(boolean showLabel) {
+ setEnabled(true);
+ }
+
+ @Override
+ public void collapse() {
+ setEnabled(false);
+ }
+
+ /**
+ * Animates a change in the content of the card
+ * @param v View in card to animate
+ * @param overlay Drawable to animate as a ViewOverlay
+ * @param duration Duration of animation
+ */
+ @Override
+ public void animateContentChange(View v, final Drawable overlay, long duration) {
+ // Since the wallpaper IS the content, we will ignore the view passed in and animate
+ // the entire card
+ final ViewOverlay viewOverlay = this.getOverlay();
+ viewOverlay.add(overlay);
+ final int x = 0;
+ final int y = 0;
+ final int width = v.getWidth();
+ final int height = v.getHeight();
+ overlay.setBounds(x, y, x + width, y + height);
+
+ final ValueAnimator overlayAnimator = ValueAnimator.ofFloat(1f, 0f);
+ overlayAnimator.setDuration(duration);
+ overlayAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float value = (Float) animation.getAnimatedValue();
+ overlay.setAlpha((int) (255 * value));
+ }
+
+ });
+ overlayAnimator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Clear out the ViewOverlay now that we are done animating
+ viewOverlay.clear();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+ });
+
+ AnimatorSet set = new AnimatorSet();
+ set.play(ObjectAnimator.ofFloat(overlay, "alpha", 0f, 1f))
+ .with(overlayAnimator);
+ set.start();
+ }
+}
diff --git a/src/org/cyanogenmod/theme/perapptheming/PerAppThemeListLayout.java b/src/org/cyanogenmod/theme/perapptheming/PerAppThemeListLayout.java
new file mode 100644
index 0000000..424d640
--- /dev/null
+++ b/src/org/cyanogenmod/theme/perapptheming/PerAppThemeListLayout.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.perapptheming;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Animation;
+import android.widget.FrameLayout;
+
+import org.cyanogenmod.theme.chooser.R;
+
+public class PerAppThemeListLayout extends FrameLayout {
+ private PerAppThemingWindow mWindow;
+ private PointF mCenter;
+
+ private float mMaxRadius;
+ private float mTargetRadius;
+ private float mStartRadius;
+ private float mCurrentRadius;
+
+ private ValueAnimator mAnimator;
+ private boolean mIsAnimating;
+
+ private Path mRevealPath;
+
+ public PerAppThemeListLayout(Context context) {
+ this(context, null);
+ }
+
+ public PerAppThemeListLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PerAppThemeListLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ final Resources res = getResources();
+ float width = res.getDimension(R.dimen.theme_list_width);
+ float height = res.getDimension(R.dimen.theme_list_max_height);
+ mMaxRadius = (float) Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
+ mRevealPath = new Path();
+
+ mAnimator = new ValueAnimator();
+ mAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
+ mAnimator.addListener(mAnimationListener);
+ mAnimator.addUpdateListener(mUpdateListener);
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN &&
+ event.getKeyCode() == KeyEvent.KEYCODE_BACK && mWindow != null) {
+ mWindow.hideThemeList();
+ }
+ return super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (isEnabled() && event.getAction() == MotionEvent.ACTION_DOWN && mWindow != null) {
+ mWindow.hideThemeList();
+ return true;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (!mIsAnimating) {
+ super.dispatchDraw(canvas);
+ } else {
+ final int state = canvas.save();
+ mRevealPath.reset();
+ mRevealPath.addCircle(mCenter.x, mCenter.y, mCurrentRadius, Path.Direction.CW);
+ canvas.clipPath(mRevealPath);
+ super.dispatchDraw(canvas);
+ canvas.restoreToCount(state);
+ }
+ }
+
+ public void setPerAppThemingWindow(PerAppThemingWindow window) {
+ mWindow = window;
+ }
+
+ /**
+ * Perform a circular reveal from center cx,cy
+ * @param cx X position of center
+ * @param cy Y position of center
+ * @param duration Duration of animation
+ */
+ public void circularReveal(float cx, float cy, long duration) {
+ mCenter = new PointF(cx, cy);
+ mIsAnimating = true;
+
+ mStartRadius = mCurrentRadius;
+ mTargetRadius = mMaxRadius;
+ startAnimation(duration);
+ }
+
+ /**
+ * Perform a circular hide from center cx,cy
+ * @param cx X position of center
+ * @param cy Y position of center
+ * @param duration Duration of animation
+ */
+ public void circularHide(float cx, float cy, long duration) {
+ mCenter = new PointF(cx, cy);
+ mIsAnimating = true;
+
+ mStartRadius = mCurrentRadius;
+ mTargetRadius = 0f;
+ startAnimation(duration);
+ }
+
+ private void startAnimation(long duration) {
+ getChildAt(0).setVisibility(View.VISIBLE);
+ mAnimator.setFloatValues(mStartRadius, mTargetRadius);
+ mAnimator.setDuration(duration);
+ mAnimator.start();
+ }
+
+ private ValueAnimator.AnimatorUpdateListener mUpdateListener =
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ Float value = (Float) animation.getAnimatedValue();
+ mCurrentRadius = value.floatValue();
+ invalidate();
+ }
+ };
+
+ private Animator.AnimatorListener mAnimationListener = new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mIsAnimating = false;
+ if (mCurrentRadius <= 0) {
+ getChildAt(0).setVisibility(INVISIBLE);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+ };
+}
diff --git a/src/org/cyanogenmod/theme/perapptheming/PerAppThemeListView.java b/src/org/cyanogenmod/theme/perapptheming/PerAppThemeListView.java
new file mode 100644
index 0000000..9986f60
--- /dev/null
+++ b/src/org/cyanogenmod/theme/perapptheming/PerAppThemeListView.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.perapptheming;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.ListView;
+
+import org.cyanogenmod.theme.chooser.R;
+import org.cyanogenmod.theme.util.Utils;
+
+public class PerAppThemeListView extends ListView {
+ private int mMinHeight;
+ private int mMaxHeight;
+
+ public PerAppThemeListView(Context context) {
+ this(context, null);
+ }
+
+ public PerAppThemeListView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PerAppThemeListView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ final Resources res = getResources();
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ Utils.getResourceDeclareStyleableIntArray("com.android.internal", "View"));
+ int resId = res.getIdentifier("View_minHeight", "styleable", "android");
+ mMinHeight = a.getDimensionPixelSize(resId, 0);
+ a.recycle();
+
+ a = context.obtainStyledAttributes(attrs, R.styleable.PerAppThemeListView);
+ mMaxHeight = a.getDimensionPixelSize(R.styleable.PerAppThemeListView_maxHeight, 0);
+ a.recycle();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // let the super do the heavy lifting and then we'll cap the values to any max and/or min
+ // values that were defined in the layout
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int measuredWidth = getMeasuredWidth();
+ int measuredHeight = getMeasuredHeight();
+ int newHeight = measuredHeight;
+ if (mMaxHeight > 0) {
+ newHeight = Math.min(measuredHeight, mMaxHeight);
+ }
+ if (mMinHeight > 0) {
+ newHeight = Math.max(newHeight, mMinHeight);
+ }
+ if (newHeight != measuredHeight) {
+ setMeasuredDimension(measuredWidth, newHeight);
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/perapptheming/PerAppThemingWindow.java b/src/org/cyanogenmod/theme/perapptheming/PerAppThemingWindow.java
new file mode 100644
index 0000000..27db329
--- /dev/null
+++ b/src/org/cyanogenmod/theme/perapptheming/PerAppThemingWindow.java
@@ -0,0 +1,1078 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.perapptheming;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.ThemeConfig;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.graphics.PixelFormat;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.IBinder;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.animation.Interpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.cyanogenmod.theme.chooser.R;
+import org.cyanogenmod.theme.util.Utils;
+
+import cyanogenmod.providers.ThemesContract.ThemesColumns;
+import cyanogenmod.themes.ThemeChangeRequest;
+import cyanogenmod.themes.ThemeManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+public class PerAppThemingWindow extends Service implements OnTouchListener,
+ ThemeManager.ThemeChangeListener {
+ // Animation frame rate per second
+ private static final int ANIMATION_FRAME_RATE = 60;
+
+ private static final int EXIT_DELETE_MODE_ANIMATION_DURATION = 50;
+
+ private static final int MOVE_TO_DELETE_BOX_ANIMATION_DURATION = 150;
+
+ private static final int ANIMATION_DURATION = 300;
+
+ private static final int FAB_SCALE_ANIMATION_DURATION = 150;
+
+ private static final int LIST_ON_LEFT_SIDE = 0;
+ private static final int LIST_ON_RIGHT_SIDE = 1;
+
+ // Don't want these colors to be themable and possibly alter the effect we are after, so
+ // they are defined here rather than in colors.xml
+ private static final int SCRIM_COLOR_TRANSPARENT = 0x00000000;
+ private static final int SCRIM_COLOR_OPAQUE = 0xaa000000;
+
+ // Amount to wait after a theme change occurred before fading the scrim away
+ // This value was obtained empirically by performing theme changes and adjusting this delay
+ private static final int THEME_CHANGE_DELAY = 1500;
+
+ private static final float PRESSED_FAB_SCALE = 0.95f;
+
+ private static final float DELETE_BOX_ANIMATION_SCALE = 0.3f;
+
+ private static final int MAX_DEPRECIATION = 5;
+
+ private static final float FAB_ANIMATION_SCALE_FACTOR = 0.44f;
+
+ // Margin around the phone
+ private static int MARGIN_VERTICAL;
+ // Margin around the phone
+ private static int MARGIN_HORIZONTAL;
+ private static int CLOSE_ANIMATION_DISTANCE;
+ private static int DRAG_DELTA;
+ private static int STARTING_POINT_Y;
+ private static int DELETE_BOX_WIDTH;
+ private static int DELETE_BOX_HEIGHT;
+ private static int FLOATING_WINDOW_ICON_SIZE;
+
+ // View variables
+ private BroadcastReceiver mBroadcastReceiver;
+ private WindowManager mWindowManager;
+ private LinearLayout mDraggableIcon;
+ private View mDraggableIconImage;
+ private WindowManager.LayoutParams mParams;
+ private PerAppThemeListLayout mThemeListLayout;
+ private WindowManager.LayoutParams mListLayoutParams;
+ private ListView mThemeList;
+ private ThemesAdapter mAdapter;
+ private FrameLayout.LayoutParams mListParams;
+ private LinearLayout mDeleteView;
+ private View mDeleteBoxView;
+ private View mThemeApplyingView;
+ private boolean mDeleteBoxVisible = false;
+ private boolean mIsDestroyed = false;
+ private boolean mIsBeingDestroyed = false;
+ private int mCurrentPosX = -1;
+
+ // Animation variables
+ private List<Float> mDeltaXArray;
+ private List<Float> mDeltaYArray;
+ private AnimationTask mAnimationTask;
+
+ // Close logic
+ private int mCurrentX;
+ private int mCurrentY;
+ private boolean mIsInDeleteMode = false;
+ private boolean mIsAnimationLocked = false;
+
+ // Drag variables
+ float mPrevDragX;
+ float mPrevDragY;
+ float mOrigX;
+ float mOrigY;
+ boolean mDragged;
+
+ private int mListSide = LIST_ON_LEFT_SIDE;
+
+ private ThemeConfig mThemeConfig;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ // Load margins, distances, etc.
+ final Resources res = getResources();
+ MARGIN_VERTICAL =
+ res.getDimensionPixelSize(R.dimen.floating_window_margin_vertical);
+ MARGIN_HORIZONTAL =
+ res.getDimensionPixelSize(R.dimen.floating_window_margin_horizontal);
+ CLOSE_ANIMATION_DISTANCE =
+ res.getDimensionPixelSize(R.dimen.floating_window_close_animation_distance);
+ DRAG_DELTA = res.getDimensionPixelSize(R.dimen.floating_window_drag_delta);
+ STARTING_POINT_Y = res.getDimensionPixelSize(R.dimen.floating_window_starting_point_y);
+
+ DELETE_BOX_WIDTH = (int) getResources().getDimension(
+ R.dimen.floating_window_delete_box_width);
+ DELETE_BOX_HEIGHT = (int) getResources().getDimension(
+ R.dimen.floating_window_delete_box_height);
+ FLOATING_WINDOW_ICON_SIZE = (int) getResources().getDimension(
+ R.dimen.floating_window_icon);
+
+ mDeleteView = new LinearLayout(getContext());
+ View.inflate(getContext(), R.layout.per_app_delete_box_window, mDeleteView);
+ mDeleteBoxView = mDeleteView.findViewById(R.id.box);
+ addView(mDeleteView, 0, 0, Gravity.BOTTOM | Gravity.CENTER_VERTICAL,
+ WindowManager.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.WRAP_CONTENT);
+ mDeleteView.setVisibility(View.GONE);
+
+ mDraggableIcon = new LinearLayout(this);
+ mDraggableIcon.setOnTouchListener(this);
+ View.inflate(getContext(), R.layout.per_app_fab_floating_window_icon, mDraggableIcon);
+ mDraggableIconImage = mDraggableIcon.findViewById(R.id.box);
+ mDraggableIconImage.setClipToOutline(true);
+ mDraggableIconImage.getViewTreeObserver().addOnWindowAttachListener(mWindowAttachListener);
+ mParams = addView(mDraggableIcon, 0, 0);
+ updateIconPosition(MARGIN_HORIZONTAL, STARTING_POINT_Y);
+
+ mThemeListLayout = (PerAppThemeListLayout) View.inflate(getContext(),
+ R.layout.per_app_theme_list, null);
+ mThemeListLayout.setPerAppThemingWindow(this);
+ mThemeList = (ListView) mThemeListLayout.findViewById(R.id.theme_list);
+ mListParams = (FrameLayout.LayoutParams) mThemeList.getLayoutParams();
+ mThemeApplyingView = mThemeListLayout.findViewById(R.id.applying_theme_text);
+
+ final Configuration config = getResources().getConfiguration();
+ mThemeConfig = getThemeConfig(config);
+ loadThemes();
+ getContentResolver().registerContentObserver(ThemesColumns.CONTENT_URI, true,
+ mThemesObserver);
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (mIsBeingDestroyed) return true;
+
+ switch(event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (mThemeListLayout.isAttachedToWindow()) {
+ hideThemeList();
+ return false;
+ }
+ mPrevDragX = mOrigX = event.getRawX();
+ mPrevDragY = mOrigY = event.getRawY();
+
+ mDragged = false;
+
+ mDeltaXArray = new LinkedList<Float>();
+ mDeltaYArray = new LinkedList<Float>();
+
+ mCurrentX = mParams.x;
+ mCurrentY = mParams.y;
+
+ mDraggableIconImage.setScaleX(PRESSED_FAB_SCALE);
+ mDraggableIconImage.setScaleY(PRESSED_FAB_SCALE);
+
+ // Cancel any currently running animations
+ if (mAnimationTask != null) {
+ mAnimationTask.cancel();
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ mIsAnimationLocked = false;
+ if (mAnimationTask != null) {
+ mAnimationTask.cancel();
+ }
+
+ if (!mDragged) {
+ // clicked so show theme list
+ final int mid = getScreenWidth() / 2;
+ mListSide = LIST_ON_LEFT_SIDE;
+ if (mCurrentPosX > mid) mListSide = LIST_ON_RIGHT_SIDE;
+ if (!mThemeListLayout.isAttachedToWindow()) showThemeList();
+ } else {
+ // Animate the icon
+ mAnimationTask = new AnimationTask();
+ mAnimationTask.run();
+ }
+
+ if (mIsInDeleteMode) {
+ close(true);
+ } else {
+ hideDeleteBox();
+ mDraggableIconImage.setScaleX(1f);
+ mDraggableIconImage.setScaleY(1f);
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ mCurrentX = (int) (event.getRawX() - mDraggableIcon.getWidth() / 2);
+ mCurrentY = (int) (event.getRawY() - mDraggableIcon.getHeight());
+ if (isDeleteMode()) {
+ mDeleteBoxView.setBackgroundResource(R.drawable.btn_quicktheme_remove_hover);
+ mIsInDeleteMode = true;
+ updateIconPosition(mCurrentX, mCurrentY);
+ } else if (mIsInDeleteMode){
+ mDeleteBoxView.setBackgroundResource(R.drawable.btn_quicktheme_remove_normal);
+ mIsInDeleteMode = false;
+ } else {
+ if(!mIsAnimationLocked && mDragged) {
+ if (mAnimationTask != null) {
+ mAnimationTask.cancel();
+ }
+ updateIconPosition(mCurrentX, mCurrentY);
+ }
+ }
+
+ float deltaX = event.getRawX() - mPrevDragX;
+ float deltaY = event.getRawY() - mPrevDragY;
+
+ mDeltaXArray.add(deltaX);
+ mDeltaYArray.add(deltaY);
+
+ mPrevDragX = event.getRawX();
+ mPrevDragY = event.getRawY();
+
+ deltaX = event.getRawX() - mOrigX;
+ deltaY = event.getRawY() - mOrigY;
+ mDragged = mDragged || Math.abs(deltaX) > DRAG_DELTA
+ || Math.abs(deltaY) > DRAG_DELTA;
+ if (mDragged) {
+ showDeleteBox();
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mIsDestroyed = true;
+ if (mDraggableIcon != null) {
+ removeViewIfAttached(mDraggableIcon);
+ mDraggableIcon = null;
+ }
+ if (mDeleteView != null) {
+ removeViewIfAttached(mDeleteView);
+ mDeleteView = null;
+ }
+ if (mThemeListLayout != null) {
+ removeViewIfAttached(mThemeListLayout);
+ mThemeListLayout = null;
+ }
+ if (mAnimationTask != null) {
+ mAnimationTask.cancel();
+ mAnimationTask = null;
+ }
+ if (mBroadcastReceiver != null) {
+ unregisterReceiver(mBroadcastReceiver);
+ mBroadcastReceiver = null;
+ }
+ if (mThemesObserver != null) {
+ getContentResolver().unregisterContentObserver(mThemesObserver);
+ mThemesObserver = null;
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mThemeConfig = getThemeConfig(newConfig);
+ }
+
+ @Override
+ public void onProgress(int progress) {
+ }
+
+ @Override
+ public void onFinish(boolean isSuccess) {
+ ThemeManager tm = ThemeManager.getInstance(getContext());
+ tm.removeClient(this);
+ mThemeListLayout.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ hideScrim();
+ startFabScaleUpAnimation();
+ }
+ }, THEME_CHANGE_DELAY);
+ }
+
+ public void hideThemeList() {
+ hideThemeList(false, new Runnable() {
+ @Override
+ public void run() {
+ removeViewIfAttached(mThemeListLayout);
+ }
+ });
+ }
+
+ private ThemeConfig getThemeConfig(Configuration config) {
+ if (config != null && config.themeConfig != null) {
+ return config.themeConfig;
+ }
+
+ return ThemeConfig.getBootTheme(getContentResolver());
+ }
+
+ private void removeViewIfAttached(View view) {
+ if (view.isAttachedToWindow()) {
+ mWindowManager.removeViewImmediate(view);
+ }
+ }
+
+ private WindowManager.LayoutParams addView(View v, int x, int y) {
+ return addView(v, x, y, Gravity.TOP | Gravity.LEFT,
+ WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
+ }
+
+ private WindowManager.LayoutParams addView(View v, int x, int y, int gravity,
+ int width, int height) {
+ mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
+ WindowManager.LayoutParams params = new WindowManager.LayoutParams(width, height,
+ WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT);
+
+ params.gravity = gravity;
+ params.x = x;
+ params.y = y;
+
+ mWindowManager.addView(v, params);
+
+ return params;
+ }
+
+ private void updateIconPosition(int x, int y) {
+ mCurrentPosX = x;
+
+ View v = mDraggableIconImage;
+ v.setTranslationX(0);
+ if (x < 0) {
+ v.setTranslationX(x);
+ x = 0;
+ }
+
+ if (x > getScreenWidth() - FLOATING_WINDOW_ICON_SIZE) {
+ v.setTranslationX(x - getScreenWidth() + FLOATING_WINDOW_ICON_SIZE);
+ x = getScreenWidth() - FLOATING_WINDOW_ICON_SIZE;
+ }
+
+ v.setTranslationY(0);
+ if (y < 0) {
+ v.setTranslationY(y);
+ y = 0;
+ }
+
+ if (y > getScreenHeight() - FLOATING_WINDOW_ICON_SIZE) {
+ v.setTranslationY(y - getScreenHeight() + FLOATING_WINDOW_ICON_SIZE);
+ y = getScreenHeight() - FLOATING_WINDOW_ICON_SIZE;
+ }
+ mParams.x = x;
+ mParams.y = y;
+
+ if (!mIsDestroyed) {
+ mWindowManager.updateViewLayout(mDraggableIcon, mParams);
+ }
+ }
+
+ private boolean isDeleteMode() {
+ return isHoveringOverDeleteBox(mParams.y);
+ }
+
+ private boolean isHoveringOverDeleteBox(int y) {
+ return y + mDraggableIconImage.getHeight() >= getScreenHeight() - DELETE_BOX_HEIGHT;
+ }
+
+ private void showDeleteBox() {
+ if (!mDeleteBoxVisible) {
+ mDeleteBoxVisible = true;
+ mDeleteView.setVisibility(View.VISIBLE);
+
+ mDeleteBoxView.setAlpha(0);
+ mDeleteBoxView.setTranslationY(CLOSE_ANIMATION_DISTANCE);
+ mDeleteBoxView.animate().alpha(1).translationYBy(-1 * CLOSE_ANIMATION_DISTANCE)
+ .setListener(null);
+
+ mDeleteBoxView.getLayoutParams().width = getScreenWidth();
+ }
+ }
+
+ private void hideDeleteBox() {
+ if (mDeleteBoxVisible) {
+ mDeleteBoxVisible = false;
+ if (mDeleteView != null) {
+ mDeleteBoxView.animate().alpha(0)
+ .translationYBy(CLOSE_ANIMATION_DISTANCE)
+ .setListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mDeleteView != null) mDeleteView.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+ }
+ }
+ }
+
+ private void animateToDeleteBoxCenter(final OnAnimationFinishedListener l) {
+ if (mIsAnimationLocked) {
+ return;
+ }
+ mIsInDeleteMode = true;
+
+ if (mAnimationTask != null) {
+ mAnimationTask.cancel();
+ }
+
+ mAnimationTask = new AnimationTask(getScreenWidth() / 2 - mDraggableIcon.getWidth() / 2,
+ getScreenHeight() - DELETE_BOX_HEIGHT / 2 - mDraggableIcon.getHeight() / 2);
+ mAnimationTask.setDuration(MOVE_TO_DELETE_BOX_ANIMATION_DURATION);
+ mAnimationTask.setAnimationFinishedListener(l);
+ mAnimationTask.run();
+ mDeleteBoxView.setBackgroundResource(R.drawable.btn_quicktheme_remove_hover);
+ }
+
+ private void close(boolean animate) {
+ if (mIsBeingDestroyed) {
+ return;
+ }
+ mIsBeingDestroyed = true;
+
+ if (animate) {
+ animateToDeleteBoxCenter(new OnAnimationFinishedListener() {
+ @Override
+ public void onAnimationFinished() {
+ hideDeleteBox();
+ mDeleteBoxView.animate()
+ .scaleX(DELETE_BOX_ANIMATION_SCALE)
+ .scaleY(DELETE_BOX_ANIMATION_SCALE);
+ mDraggableIconImage.animate()
+ .scaleX(DELETE_BOX_ANIMATION_SCALE)
+ .scaleY(DELETE_BOX_ANIMATION_SCALE)
+ .translationY(CLOSE_ANIMATION_DISTANCE)
+ .setDuration(mDeleteBoxView.animate().getDuration())
+ .setListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ stopSelf();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+ }
+ });
+ } else {
+ stopSelf();
+ }
+ }
+
+ private static interface OnAnimationFinishedListener {
+ public void onAnimationFinished();
+ }
+
+ private Context getContext() {
+ return this;
+ }
+
+ private int getScreenWidth() {
+ return getResources().getDisplayMetrics().widthPixels;
+ }
+
+ private int getScreenHeight() {
+ return getResources().getDisplayMetrics().heightPixels - getStatusBarHeight();
+ }
+
+ private int getStatusBarHeight() {
+ int result = 0;
+ int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
+ if (resourceId > 0) {
+ result = getResources().getDimensionPixelSize(resourceId);
+ }
+
+ return result;
+ }
+
+ private void loadThemes() {
+ String[] columns = {ThemesColumns._ID, ThemesColumns.TITLE, ThemesColumns.PKG_NAME};
+ String selection = ThemesColumns.MODIFIES_OVERLAYS + "=? AND " +
+ ThemesColumns.INSTALL_STATE + "=?";
+ String[] selectionArgs = {"1", "" + ThemesColumns.InstallState.INSTALLED};
+ String sortOrder = ThemesColumns.TITLE + " ASC";
+ Cursor c = getContentResolver().query(ThemesColumns.CONTENT_URI, columns, selection,
+ selectionArgs, sortOrder);
+ if (c != null) {
+ if (mAdapter == null) {
+ mAdapter = new ThemesAdapter(this, c);
+ mThemeList.setAdapter(mAdapter);
+ mThemeList.setOnItemClickListener(mThemeClickedListener);
+ } else {
+ String pkgName = (String) mAdapter.getItem(0);
+ mAdapter.populateThemes(c);
+ mAdapter.setCurrentTheme(pkgName);
+ }
+ }
+ }
+
+ private ContentObserver mThemesObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ onChange(selfChange, null);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ loadThemes();
+ }
+ };
+
+ private ViewTreeObserver.OnWindowAttachListener mWindowAttachListener =
+ new ViewTreeObserver.OnWindowAttachListener() {
+ @Override
+ public void onWindowAttached() {
+ // Remove the this OnWindowAttachListener now that we are done with it.
+ mDraggableIconImage.getViewTreeObserver().removeOnWindowAttachListener(this);
+
+ final float fabWidth = getResources().getDimension(R.dimen.floating_window_icon);
+ mDraggableIconImage.setAlpha(0);
+ mDraggableIconImage.setX(-fabWidth);
+ mDraggableIconImage.animate()
+ .alpha(1f)
+ .xBy(fabWidth)
+ .setDuration(ANIMATION_DURATION)
+ .start();
+ }
+
+ @Override
+ public void onWindowDetached() {
+ }
+ };
+
+ private void showThemeList() {
+ if (mListLayoutParams == null) {
+ mListLayoutParams = new WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.TYPE_PHONE,
+ WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
+ WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED |
+ WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+ PixelFormat.TRANSLUCENT);
+ }
+ mListLayoutParams.gravity = Gravity.TOP |
+ (mListSide == LIST_ON_LEFT_SIDE ? Gravity.LEFT : Gravity.RIGHT);
+ mWindowManager.addView(mThemeListLayout, mListLayoutParams);
+
+ setThemeListPosition();
+ startFabScaleDownAnimation();
+
+ mAdapter.setCurrentTheme(
+ mThemeConfig.getOverlayPkgNameForApp(Utils.getTopTaskPackageName(this)));
+ mThemeListLayout.circularReveal(mParams.x + mDraggableIconImage.getWidth() / 2,
+ mParams.y + mDraggableIconImage.getHeight() / 2, ANIMATION_DURATION);
+ }
+
+ private void hideThemeList(boolean showScrim, final Runnable endAction) {
+ if (showScrim) {
+ showScrim();
+ } else {
+ startFabScaleUpAnimation();
+ }
+ mThemeListLayout.circularHide(mParams.x + mDraggableIconImage.getWidth() / 2,
+ mParams.y + mDraggableIconImage.getHeight() / 2, ANIMATION_DURATION);
+ if (endAction != null) {
+ mDraggableIcon.postDelayed(endAction, ANIMATION_DURATION);
+ }
+ }
+
+ private void showScrim() {
+ ValueAnimator animator = ValueAnimator.ofArgb(SCRIM_COLOR_TRANSPARENT,
+ SCRIM_COLOR_OPAQUE);
+ mThemeListLayout.setEnabled(false);
+ animator.setDuration(ANIMATION_DURATION)
+ .addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ Integer value = (Integer) animation.getAnimatedValue();
+ mThemeListLayout.setBackgroundColor(value.intValue());
+ }
+ });
+ animator.start();
+ mThemeApplyingView.animate()
+ .alpha(1f)
+ .setDuration(ANIMATION_DURATION);
+ }
+
+ private void hideScrim() {
+ ValueAnimator animator = ValueAnimator.ofArgb(SCRIM_COLOR_OPAQUE, SCRIM_COLOR_TRANSPARENT);
+ animator.setDuration(ANIMATION_DURATION)
+ .addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ Integer value = (Integer) animation.getAnimatedValue();
+ mThemeListLayout.setBackgroundColor(value.intValue());
+ }
+ });
+ animator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ removeViewIfAttached(mThemeListLayout);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+ animator.start();
+ mThemeApplyingView.animate()
+ .alpha(0f)
+ .setDuration(ANIMATION_DURATION);
+ mDraggableIcon.setVisibility(View.VISIBLE);
+ mDraggableIconImage.animate()
+ .alpha(1f)
+ .setDuration(ANIMATION_DURATION);
+ }
+
+ private void setThemeListPosition() {
+ int thirdHeight = getScreenHeight() / 3;
+ // use the center of the fab to decide where to place the list
+ int fabLocationY = mParams.y + mDraggableIconImage.getHeight() / 2;
+ int listHeight = mThemeList.getMeasuredHeight();
+ if (listHeight <= 0) {
+ // not measured yet so let's force that
+ int width = getResources().getDimensionPixelSize(R.dimen.theme_list_width);
+ int height = getResources().getDimensionPixelSize(R.dimen.theme_list_max_height);
+ mThemeList.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.AT_MOST),
+ View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST));
+ listHeight = mThemeList.getMeasuredHeight();
+ }
+
+ // If we're in the top 1/3 of the screen position the top of the list with the top
+ // of the fab. Second 3rd will position the list so that it is vertically centered
+ // with the fab center. Bottom 3rd will position the bottom of the list with the
+ // bottom of the fab.
+ if (fabLocationY < thirdHeight) {
+ mListParams.topMargin = mParams.y + mDraggableIconImage.getHeight() / 2;
+ } else if (fabLocationY < thirdHeight * 2) {
+ mListParams.topMargin = fabLocationY - listHeight / 2;
+ } else {
+ mListParams.topMargin = mParams.y + mDraggableIconImage.getHeight() / 2 - listHeight;
+ }
+ mListParams.gravity = Gravity.TOP |
+ (mListSide == LIST_ON_LEFT_SIDE ? Gravity.LEFT : Gravity.RIGHT);
+ mThemeList.setLayoutParams(mListParams);
+ }
+
+ private void startFabScaleDownAnimation() {
+ final int iconWidth = mDraggableIconImage.getWidth();
+ final float translateX = (iconWidth - (float) iconWidth * FAB_ANIMATION_SCALE_FACTOR) / 2 *
+ (mListSide == LIST_ON_LEFT_SIDE ? -1 : 1);
+
+ mDraggableIconImage.animate()
+ .scaleX(FAB_ANIMATION_SCALE_FACTOR)
+ .scaleY(FAB_ANIMATION_SCALE_FACTOR)
+ .translationXBy(translateX)
+ .setDuration(FAB_SCALE_ANIMATION_DURATION);
+ }
+
+ private void startFabScaleUpAnimation() {
+ final float iconWidth = mDraggableIconImage.getWidth();
+ final float translateX = (iconWidth - (float) iconWidth * FAB_ANIMATION_SCALE_FACTOR) / 2 *
+ (mListSide == LIST_ON_LEFT_SIDE ? 1 : -1);
+
+ mDraggableIconImage.animate()
+ .scaleX(1f)
+ .scaleY(1f)
+ .translationXBy(translateX)
+ .setDuration(FAB_SCALE_ANIMATION_DURATION);
+ }
+
+ private AdapterView.OnItemClickListener mThemeClickedListener =
+ new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
+ final String themePkgName = (String) view.getTag(R.id.tag_key_name);
+ final String appPkgName = Utils.getTopTaskPackageName(getContext());
+ if (!TextUtils.isEmpty(appPkgName) && !TextUtils.isEmpty(themePkgName)) {
+ if (!Utils.themeHasOverlayForApp(getContext(), appPkgName, themePkgName)) {
+ Toast.makeText(getContext(), R.string.per_app_theme_app_not_overlaid_warning,
+ Toast.LENGTH_LONG).show();
+ }
+ hideThemeList(true, new Runnable() {
+ @Override
+ public void run() {
+ ThemeManager tm = ThemeManager.getInstance(getContext());
+ ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder();
+ builder.setAppOverlay(appPkgName, themePkgName);
+ try {
+ tm.addClient(PerAppThemingWindow.this);
+ } catch (IllegalArgumentException e) {
+ /* ignore since this means we already have a listener added */
+ }
+ tm.requestThemeChange(builder.build(), false);
+ }
+ });
+ } else {
+ hideThemeList();
+ }
+ }
+ };
+
+ private float calculateVelocityX() {
+ int depreciation = mDeltaXArray.size() + 1;
+ float sum = 0;
+ for (Float f : mDeltaXArray) {
+ depreciation--;
+ if (depreciation > MAX_DEPRECIATION){
+ continue;
+ }
+
+ sum += f / depreciation;
+ }
+
+ return sum;
+ }
+
+ private float calculateVelocityY() {
+ int depreciation = mDeltaYArray.size() + 1;
+ float sum = 0;
+ for (Float f : mDeltaYArray) {
+ depreciation--;
+ if (depreciation > 5) {
+ continue;
+ }
+
+ sum += f / depreciation;
+ }
+
+ return sum;
+ }
+
+ // Timer for animation/automatic movement of the tray
+ private class AnimationTask {
+ // Ultimate destination coordinates toward which the view will move
+ int mDestX;
+ int mDestY;
+ long mDuration = 350;
+ long mStartTime;
+ float mTension = 1.4f;
+ Interpolator mInterpolator = new OvershootInterpolator(mTension);
+ long mSteps;
+ long mCurrentStep;
+ int mDistX;
+ int mOrigX;
+ int mDistY;
+ int mOrigY;
+ Handler mAnimationHandler = new Handler();
+ OnAnimationFinishedListener mAnimationFinishedListener;
+
+ public AnimationTask(int x, int y) {
+ setup(x, y);
+ }
+
+ public AnimationTask() {
+ setup(calculateX(), calculateY());
+
+ float velocityX = calculateVelocityX();
+ float velocityY = calculateVelocityY();
+ mTension += Math.sqrt(velocityX * velocityX + velocityY * velocityY) / 200;
+ mInterpolator = new OvershootInterpolator(mTension);
+ }
+
+ private void setup(int x, int y) {
+ if (mIsAnimationLocked) {
+ throw new RuntimeException("Returning to user's finger. Avoid animations while " +
+ "mIsAnimationLocked flag is set.");
+ }
+
+ mDestX = x;
+ mDestY = y;
+
+ mSteps = (int) (((float) mDuration) / 1000 * ANIMATION_FRAME_RATE);
+ mCurrentStep = 1;
+ mDistX = mParams.x - mDestX;
+ mOrigX = mParams.x;
+ mDistY = mParams.y - mDestY;
+ mOrigY = mParams.y;
+ }
+
+ public long getDuration() {
+ return mDuration;
+ }
+
+ public void setDuration(long duration) {
+ mDuration = duration;
+ setup(mDestX, mDestY);
+ }
+
+ public OnAnimationFinishedListener getAnimationFinishedListener() {
+ return mAnimationFinishedListener;
+ }
+
+ public void setAnimationFinishedListener(OnAnimationFinishedListener l) {
+ mAnimationFinishedListener = l;
+ }
+
+ public Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ public void setInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ }
+
+ private int calculateX() {
+ float velocityX = calculateVelocityX();
+ int screenWidth = getScreenWidth();
+ int destX = (mParams.x + mDraggableIcon.getWidth() / 2 > screenWidth / 2)
+ ? screenWidth - mDraggableIcon.getWidth() - MARGIN_HORIZONTAL
+ : 0 + MARGIN_HORIZONTAL;
+
+ if (Math.abs(velocityX) > 50) {
+ destX = (velocityX > 0) ? screenWidth - mDraggableIcon.getWidth()
+ - MARGIN_HORIZONTAL : 0 + MARGIN_HORIZONTAL;
+ }
+
+ return destX;
+ }
+
+ private int calculateY() {
+ float velocityY = calculateVelocityY();
+ mInterpolator = new OvershootInterpolator(mTension);
+ int screenHeight = getScreenHeight();
+ int destY = mParams.y + (int) (velocityY * 3);
+ if (destY <= 0) {
+ destY = MARGIN_VERTICAL;
+ }
+ if (destY >= screenHeight - mDraggableIcon.getHeight()) {
+ destY = screenHeight - mDraggableIcon.getHeight() - MARGIN_VERTICAL;
+ }
+
+ return destY;
+ }
+
+ public void run() {
+ mStartTime = System.currentTimeMillis();
+ for (mCurrentStep = 1; mCurrentStep <= mSteps; mCurrentStep++) {
+ long delay = mCurrentStep * mDuration / mSteps;
+ final float currentStep = mCurrentStep;
+ mAnimationHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ // Update coordinates of the view
+ float percent = mInterpolator.getInterpolation(currentStep / mSteps);
+ updateIconPosition(mOrigX - (int) (percent * mDistX), mOrigY
+ - (int) (percent * mDistY));
+
+ // Notify the animation has ended
+ if (currentStep >= mSteps) {
+ if (mAnimationFinishedListener != null) mAnimationFinishedListener
+ .onAnimationFinished();
+ }
+ }
+ }, delay);
+ }
+ }
+
+ public void cancel() {
+ mAnimationHandler.removeCallbacksAndMessages(null);
+ mAnimationTask = null;
+ }
+ }
+
+ /**
+ * We're extending BaseAdapter rather than CursorAdapter so that we can quickly re-order
+ * the list without needing to requery the provider. We're only storing the package name
+ * and theme title so there is minimum memory impact on doing this.
+ */
+ class ThemesAdapter extends BaseAdapter {
+ private static final float HALF_OPACITY = 0.5f;
+ private static final float FULL_OPACITY = 1.0f;
+
+ private ArrayList<ThemeInfo> mThemes;
+ private LayoutInflater mInflater;
+
+ public ThemesAdapter(Context context, Cursor cursor) {
+ mInflater = (LayoutInflater) context.getSystemService(LAYOUT_INFLATER_SERVICE);
+ mThemes = new ArrayList<ThemeInfo>(cursor.getCount());
+ populateThemes(cursor);
+ cursor.close();
+ }
+
+ @Override
+ public int getCount() {
+ return mThemes.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mThemes.get(position).pkgName;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.per_app_theme_list_item, parent, false);
+ Holder holder = new Holder();
+ holder.title = (TextView) convertView.findViewById(R.id.theme_title);
+ holder.indicator = (TextView) convertView.findViewById(R.id.selected_indicator);
+ convertView.setTag(R.id.tag_key_holder, holder);
+ }
+ ThemeInfo themeInfo = mThemes.get(position);
+ Holder holder = (Holder) convertView.getTag(R.id.tag_key_holder);
+ holder.title.setText(themeInfo.title);
+ if (position == 0) {
+ holder.title.setAlpha(HALF_OPACITY);
+ holder.indicator.setVisibility(View.VISIBLE);
+ convertView.setEnabled(false);
+ } else {
+ holder.title.setAlpha(FULL_OPACITY);
+ holder.indicator.setVisibility(View.INVISIBLE);
+ convertView.setEnabled(true);
+ }
+ convertView.setTag(R.id.tag_key_name, themeInfo.pkgName);
+ return convertView;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return position != 0;
+ }
+
+ public void setCurrentTheme(String pkgName) {
+ ThemeInfo info = null;
+ for (ThemeInfo ti : mThemes) {
+ if (ti.pkgName.equals(pkgName)) {
+ info = ti;
+ break;
+ }
+ }
+ if (info != null) {
+ Collections.sort(mThemes);
+ mThemes.remove(info);
+ mThemes.add(0, info);
+ notifyDataSetChanged();
+ }
+ }
+
+ private void populateThemes(Cursor cursor) {
+ mThemes.clear();
+ while(cursor.moveToNext()) {
+ ThemeInfo info = new ThemeInfo(
+ cursor.getString(cursor.getColumnIndex(ThemesColumns.PKG_NAME)),
+ cursor.getString(cursor.getColumnIndex(ThemesColumns.TITLE)));
+ mThemes.add(info);
+ }
+ }
+
+ private class Holder {
+ TextView title;
+ TextView indicator;
+ }
+
+ private class ThemeInfo implements Comparable {
+ String pkgName;
+ String title;
+
+ public ThemeInfo(String pkgName, String title) {
+ this.pkgName = pkgName;
+ this.title = title;
+ }
+
+ @Override
+ public int compareTo(Object another) {
+ return this.title.compareTo(((ThemeInfo)another).title);
+ }
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/AudioUtils.java b/src/org/cyanogenmod/theme/util/AudioUtils.java
new file mode 100644
index 0000000..c4ad68e
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/AudioUtils.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.AssetManager;
+import android.content.res.ThemeConfig;
+import android.media.MediaPlayer;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.util.Log;
+
+import org.cyanogenmod.internal.util.ThemeUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+public class AudioUtils {
+ private static final String TAG = AudioUtils.class.getSimpleName();
+
+ public static void loadThemeAudible(Context context, int type, String pkgName, MediaPlayer mp)
+ throws PackageManager.NameNotFoundException {
+ if (ThemeConfig.SYSTEM_DEFAULT.equals(pkgName)) {
+ loadSystemAudible(type, mp);
+ return;
+ }
+ PackageInfo pi = context.getPackageManager().getPackageInfo(pkgName, 0);
+ Context themeCtx = context.createPackageContext(pkgName, 0);
+ 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]);
+ if (mp != null) {
+ mp.reset();
+ mp.setDataSource(afd.getFileDescriptor(),
+ afd.getStartOffset(), afd.getLength());
+ mp.prepare();
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to load sound for " + pkgName, e);
+ }
+ }
+ }
+
+ public static void loadSystemAudible(int type, MediaPlayer mp) {
+ final String audiblePath = ThemeUtils.getDefaultAudiblePath(type);
+ if (audiblePath != null && (new File(audiblePath)).exists()) {
+ try {
+ mp.reset();
+ mp.setDataSource(audiblePath);
+ mp.prepare();
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to load system sound " + audiblePath, e);
+ }
+ }
+ }
+
+ public static Uri loadDefaultAudible(Context context, int type, MediaPlayer mp)
+ throws IOException {
+ Uri ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, type);
+ if (ringtoneUri != null) {
+ mp.reset();
+ mp.setDataSource(context, ringtoneUri);
+ mp.prepare();
+ }
+
+ return ringtoneUri;
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/BootAnimationHelper.java b/src/org/cyanogenmod/theme/util/BootAnimationHelper.java
new file mode 100644
index 0000000..1bd4ee7
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/BootAnimationHelper.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.content.res.ThemeConfig;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.AsyncTask;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+
+public class BootAnimationHelper {
+ private static final String TAG = BootAnimationHelper.class.getSimpleName();
+ private static final int MAX_REPEAT_COUNT = 3;
+
+ 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";
+ public static final String CACHED_SUFFIX = "_bootanimation.zip";
+
+ public static final int NUM_FIRST_LINE_PARAMETERS = 3;
+ public static final int NUM_PART_LINE_PARAMETERS = 4;
+
+ public static class AnimationPart {
+ /**
+ * Number of times to play this part
+ */
+ public int playCount;
+ /**
+ * If non-zero, pause for the given # of seconds before moving on to next part.
+ */
+ public int pause;
+ /**
+ * The name of this part
+ */
+ public String partName;
+ /**
+ * Time each frame is displayed
+ */
+ public int frameRateMillis;
+ /**
+ * List of file names for the given frames in this part
+ */
+ public List<String> frames;
+ /**
+ * width of the animation
+ */
+ public int width;
+ /**
+ * height of the animation
+ */
+ public int height;
+
+ public AnimationPart(int playCount, int pause, String partName, int frameRateMillis,
+ int width, int height) {
+ this.playCount = playCount == 0 ? MAX_REPEAT_COUNT : playCount;
+ this.pause = pause;
+ this.partName = partName;
+ this.frameRateMillis = frameRateMillis;
+ this.width = width;
+ this.height = height;
+ frames = new ArrayList<String>();
+ }
+
+ public void addFrame(String frame) {
+ frames.add(frame);
+ }
+ }
+
+ /**
+ * Gather up all the details for the given boot animation
+ * @param zip The bootanimation.zip
+ * @return A list of AnimationPart if successful, null if not.
+ * @throws IOException
+ */
+ public static List<AnimationPart> parseAnimation(ZipFile zip)
+ throws IOException, BootAnimationException {
+ if (zip == null) {
+ // To make tracking down boot animation problems we'll throw a BootAnimationException
+ // instead of an IllegalArgumentException.
+ throw new BootAnimationException("Boot animation ZipFile cannot be null");
+ }
+ List<AnimationPart> animationParts = null;
+
+ ZipEntry ze = zip.getEntry("desc.txt");
+ if (ze != null) {
+ animationParts = parseDescription(zip.getInputStream(ze));
+ } else {
+ throw new BootAnimationException("Missing desc.txt in root of bootanimation.zip");
+ }
+
+ if (animationParts == null) {
+ // We really should not end up here but in case we do here's an exception for ya!
+ throw new BootAnimationException("Unable to load boot animation.");
+ }
+
+ Iterator<AnimationPart> iterator = animationParts.iterator();
+ while(iterator.hasNext()) {
+ AnimationPart a = iterator.next();
+ for (Enumeration<? extends ZipEntry> e = zip.entries();e.hasMoreElements();) {
+ ze = e.nextElement();
+ if (!ze.isDirectory() && ze.getName().contains(a.partName)) {
+ a.addFrame(ze.getName());
+ }
+ }
+ if (a.frames.size() > 0) {
+ Collections.sort(a.frames);
+ } else {
+ // This boot animation may be salvageable if there are still some other parts
+ // that are good. We'll remove this part and if there are no parts left by
+ // the time we have iterated over all the parts then we can throw an exception.
+ Log.w(TAG, String.format("No frames in part %s, removing from animation",
+ a.partName));
+ iterator.remove();
+ }
+ }
+ if (animationParts.size() == 0) {
+ throw new BootAnimationException("Boot animation must have at least one part.");
+ }
+
+ return animationParts;
+ }
+
+ /**
+ * 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, BootAnimationException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+
+ // read in suggested width, height, and frame rate from first line
+ String line = reader.readLine();
+ String[] details = line.split(" ");
+ if (details.length != NUM_FIRST_LINE_PARAMETERS) {
+ throw new BootAnimationException(String.format(
+ "Invalid # of parameters on first line of desc.txt; exptected %d, read %d " +
+ "(\"%s\")",
+ NUM_FIRST_LINE_PARAMETERS, details.length, line));
+ }
+
+ // The items should be in the following order: width, height, frame rate
+ final int width = Integer.parseInt(details[0]);
+ final int height = Integer.parseInt(details[1]);
+ final int frameRateMillis = 1000 / Integer.parseInt(details[2]);
+
+ List<AnimationPart> animationParts = new ArrayList<AnimationPart>();
+ while ((line = reader.readLine()) != null) {
+ // trim off any leading and trailing spaces
+ line = line.trim();
+ // if the line is empty continue on to the next
+ if (TextUtils.isEmpty(line)) continue;
+
+ String[] info = line. split(" ");
+ if (info.length != NUM_PART_LINE_PARAMETERS) {
+ Log.w(TAG, String.format(
+ "Invalid # of part parameters; exptected %d, read %d (\"%s\")",
+ NUM_PART_LINE_PARAMETERS, info.length, line));
+ // let's continue in case there are parts that are valid
+ continue;
+ }
+ if (!info[0].equals("p") && !info[0].equals("c")) {
+ Log.w(TAG, String.format(
+ "Unknown part type; expected 'p' or 'c', read %s (\"%s\")", info[0], line));
+
+ // let's continue in case there are parts that are valid
+ continue;
+ }
+ int playCount = Integer.parseInt(info[1]);
+ int pause = Integer.parseInt(info[2]);
+ String name = info[3];
+ AnimationPart ap = new AnimationPart(playCount, pause, name, frameRateMillis,
+ width, height);
+ animationParts.add(ap);
+ }
+
+ return animationParts;
+ }
+
+ 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 void clearBootAnimationCache(Context context) {
+ File cache = context.getCacheDir();
+ if (cache.exists()) {
+ for(File f : cache.listFiles()) {
+ // volley stores stuff in cache so don't delete the volley directory
+ if(!f.isDirectory() && f.getName().endsWith(CACHED_SUFFIX)) f.delete();
+ }
+ }
+ }
+
+ 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 (ThemeConfig.SYSTEM_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);
+ }
+ }
+ }
+
+ public static class BootAnimationException extends Exception {
+ public BootAnimationException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/CursorLoaderHelper.java b/src/org/cyanogenmod/theme/util/CursorLoaderHelper.java
new file mode 100644
index 0000000..7fcd786
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/CursorLoaderHelper.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.database.Cursor;
+import android.net.Uri;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+
+import cyanogenmod.app.ThemeVersion;
+import cyanogenmod.providers.ThemesContract;
+import cyanogenmod.providers.ThemesContract.PreviewColumns;
+import cyanogenmod.providers.ThemesContract.ThemesColumns;
+
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_ALARMS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_BOOT_ANIM;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_LAUNCHER;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_LOCKSCREEN;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_NOTIFICATIONS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_OVERLAYS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_RINGTONES;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_STATUS_BAR;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_NAVIGATION_BAR;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_ICONS;
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.MODIFIES_FONTS;
+
+public class CursorLoaderHelper {
+
+ public static final int LOADER_ID_INVALID = -1;
+ public static final int LOADER_ID_ALL = 0;
+ public static final int LOADER_ID_STATUS_BAR = 1;
+ public static final int LOADER_ID_FONT = 2;
+ public static final int LOADER_ID_ICONS = 3;
+ public static final int LOADER_ID_WALLPAPER = 4;
+ public static final int LOADER_ID_NAVIGATION_BAR = 5;
+ public static final int LOADER_ID_LOCKSCREEN = 6;
+ public static final int LOADER_ID_STYLE = 7;
+ public static final int LOADER_ID_BOOT_ANIMATION = 8;
+ public static final int LOADER_ID_RINGTONE = 9;
+ public static final int LOADER_ID_NOTIFICATION = 10;
+ public static final int LOADER_ID_ALARM = 11;
+ public static final int LOADER_ID_LIVE_LOCK_SCREEN = 12;
+ public static final int LOADER_ID_INSTALLED_THEMES = 1000;
+ public static final int LOADER_ID_APPLIED = 1001;
+
+ private static final long DEFAULT_COMPONENT_ID = 0;
+
+ private static int mThemeVersion = ThemeVersion.getVersion();
+
+ public static Loader<Cursor> chooserActivityCursorLoader(Context context, int id,
+ String appliedBaseTheme) {
+ String selection = null;
+ String selectionArgs[] = null;
+ String sortOrder = null;
+ String[] projection = null;
+ Uri contentUri = null;
+
+ switch (id) {
+ case LOADER_ID_INSTALLED_THEMES:
+ selection = ThemesColumns.PRESENT_AS_THEME + "=? AND " +
+ ThemesColumns.INSTALL_STATE + "=?";
+ selectionArgs = new String[] { "1", "" + ThemesColumns.InstallState.INSTALLED};
+ // sort in ascending order but make sure the "default" theme is always first
+ sortOrder = "(" + ThemesColumns.IS_DEFAULT_THEME + "=1) DESC, "
+ + "(" + ThemesColumns.PKG_NAME + "='" + appliedBaseTheme + "') DESC, "
+ + ThemesColumns.INSTALL_TIME + " DESC";
+ contentUri = ThemesColumns.CONTENT_URI;
+ projection = new String[] {ThemesColumns.PKG_NAME, ThemesColumns.TITLE,
+ ThemesColumns.AUTHOR};
+ break;
+ case LOADER_ID_APPLIED:
+ //TODO: Mix n match query should only be done once
+ contentUri = ThemesContract.MixnMatchColumns.CONTENT_URI;
+ selection = null;
+ selectionArgs = null;
+ break;
+ }
+
+ return new CursorLoader(context, contentUri, projection, selection,
+ selectionArgs, sortOrder);
+ }
+
+ public static Loader<Cursor> componentSelectorCursorLoader(Context context, int id) {
+ Uri uri = PreviewColumns.CONTENT_URI;
+ String selection;
+ String[] selectionArgs = { "1" };
+ String[] projection = { ThemesColumns.TITLE, ThemesColumns.PKG_NAME };
+ switch(id) {
+ case LOADER_ID_STATUS_BAR:
+ selection = MODIFIES_STATUS_BAR + "=?";
+ projection = new String[] {
+ PreviewColumns.STATUSBAR_WIFI_ICON,
+ PreviewColumns.STATUSBAR_SIGNAL_ICON,
+ PreviewColumns.STATUSBAR_BLUETOOTH_ICON,
+ PreviewColumns.STATUSBAR_BACKGROUND,
+ PreviewColumns.STATUSBAR_BATTERY_CIRCLE,
+ PreviewColumns.STATUSBAR_BATTERY_LANDSCAPE,
+ PreviewColumns.STATUSBAR_BATTERY_PORTRAIT,
+ ThemesColumns.TITLE,
+ ThemesColumns.PKG_NAME
+ };
+ break;
+ case LOADER_ID_NAVIGATION_BAR:
+ selection = MODIFIES_NAVIGATION_BAR + "=?";
+ projection = new String[] {
+ PreviewColumns.NAVBAR_BACK_BUTTON,
+ PreviewColumns.STATUSBAR_BACKGROUND,
+ ThemesColumns.TITLE,
+ ThemesColumns.PKG_NAME,
+ };
+ break;
+ case LOADER_ID_FONT:
+ // fonts don't have generated previews so use the ThemesColumns.CONTENT_URI
+ uri = ThemesColumns.CONTENT_URI;
+ selection = MODIFIES_FONTS + "=?";
+ break;
+ case LOADER_ID_ICONS:
+ selection = MODIFIES_ICONS + "=?";
+ projection = new String[] {
+ PreviewColumns.ICON_PREVIEW_1,
+ ThemesColumns.TITLE,
+ ThemesColumns.PKG_NAME
+ };
+ break;
+ case LOADER_ID_STYLE:
+ selection = MODIFIES_OVERLAYS + "=?";
+ projection = new String[] {
+ PreviewColumns.STYLE_THUMBNAIL,
+ ThemesColumns.TITLE,
+ ThemesColumns.PKG_NAME
+ };
+ break;
+ case LOADER_ID_WALLPAPER:
+ selection = MODIFIES_LAUNCHER + "=?";
+ if (mThemeVersion >= 3) {
+ uri = PreviewColumns.COMPONENTS_URI;
+ projection = new String[]{
+ PreviewColumns.WALLPAPER_THUMBNAIL,
+ ThemesColumns.TITLE,
+ ThemesColumns.PKG_NAME,
+ PreviewColumns.COMPONENT_ID
+ };
+ } else {
+ projection = new String[]{
+ PreviewColumns.WALLPAPER_THUMBNAIL,
+ ThemesColumns.TITLE,
+ ThemesColumns.PKG_NAME
+ };
+ }
+ break;
+ case LOADER_ID_BOOT_ANIMATION:
+ selection = MODIFIES_BOOT_ANIM + "=?";
+ projection = new String[] {
+ PreviewColumns.BOOTANIMATION_THUMBNAIL,
+ ThemesColumns.TITLE,
+ ThemesColumns.PKG_NAME
+ };
+ break;
+ case LOADER_ID_RINGTONE:
+ selection = MODIFIES_RINGTONES + "=?";
+ break;
+ case LOADER_ID_NOTIFICATION:
+ selection = MODIFIES_NOTIFICATIONS + "=?";
+ break;
+ case LOADER_ID_ALARM:
+ selection = MODIFIES_ALARMS + "=?";
+ break;
+ case LOADER_ID_LOCKSCREEN:
+ selection = MODIFIES_LOCKSCREEN + "=?";
+ selectionArgs = new String[] { "1" };
+ if (mThemeVersion >= 3) {
+ projection = new String[]{
+ PreviewColumns.LOCK_WALLPAPER_THUMBNAIL,
+ PreviewColumns.LIVE_LOCK_SCREEN_THUMBNAIL,
+ ThemesColumns.TITLE,
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN,
+ PreviewColumns.COMPONENT_ID
+ };
+ } else {
+ projection = new String[]{
+ PreviewColumns.LOCK_WALLPAPER_THUMBNAIL,
+ PreviewColumns.LIVE_LOCK_SCREEN_THUMBNAIL,
+ ThemesColumns.TITLE,
+ ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN,
+ ThemesColumns.PKG_NAME
+ };
+ }
+ break;
+ case LOADER_ID_LIVE_LOCK_SCREEN:
+ selection = MODIFIES_LIVE_LOCK_SCREEN + "=?";
+ selectionArgs = new String[] { "1" };
+ if (mThemeVersion >= 3) {
+ projection = new String[]{
+ PreviewColumns.LIVE_LOCK_SCREEN_THUMBNAIL,
+ ThemesColumns.TITLE,
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN,
+ PreviewColumns.COMPONENT_ID
+ };
+ } else {
+ projection = new String[]{
+ PreviewColumns.LOCK_WALLPAPER_THUMBNAIL,
+ PreviewColumns.LIVE_LOCK_SCREEN_THUMBNAIL,
+ ThemesColumns.TITLE,
+ ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN,
+ ThemesColumns.PKG_NAME
+ };
+ }
+ break;
+ default:
+ return null;
+ }
+ // sort in ascending order but make sure the "default" theme is always first
+ String sortOrder = "(" + ThemesContract.ThemesColumns.IS_DEFAULT_THEME + "=1) DESC, "
+ + ThemesContract.ThemesColumns.TITLE + " ASC";
+ return new CursorLoader(context, uri, projection, selection, selectionArgs, sortOrder);
+ }
+
+ public static Loader<Cursor> myThemeFragmentCursorLoader(Context context, int id) {
+ Uri uri;
+ String[] projection;
+ projection = new String[]{
+ PreviewColumns.WALLPAPER_PREVIEW,
+ PreviewColumns.STATUSBAR_BACKGROUND,
+ PreviewColumns.STATUSBAR_WIFI_ICON,
+ PreviewColumns.STATUSBAR_WIFI_COMBO_MARGIN_END,
+ PreviewColumns.STATUSBAR_BLUETOOTH_ICON,
+ PreviewColumns.STATUSBAR_SIGNAL_ICON,
+ PreviewColumns.STATUSBAR_CLOCK_TEXT_COLOR,
+ PreviewColumns.STATUSBAR_BATTERY_CIRCLE,
+ PreviewColumns.STATUSBAR_BATTERY_LANDSCAPE,
+ PreviewColumns.STATUSBAR_BATTERY_PORTRAIT,
+ PreviewColumns.NAVBAR_BACK_BUTTON,
+ PreviewColumns.NAVBAR_HOME_BUTTON,
+ PreviewColumns.NAVBAR_RECENT_BUTTON,
+ PreviewColumns.ICON_PREVIEW_1,
+ PreviewColumns.ICON_PREVIEW_2,
+ PreviewColumns.ICON_PREVIEW_3,
+ PreviewColumns.LOCK_WALLPAPER_PREVIEW,
+ PreviewColumns.STYLE_PREVIEW,
+ PreviewColumns.NAVBAR_BACKGROUND,
+ PreviewColumns.LIVE_LOCK_SCREEN_PREVIEW
+ };
+ uri = PreviewColumns.APPLIED_URI;
+ return new CursorLoader(context, uri, projection, null, null, null);
+ }
+
+ public static Loader<Cursor> themeFragmentCursorLoader(Context context, int id, String pkgName,
+ long componentId) {
+ Uri uri = PreviewColumns.CONTENT_URI;
+ String selection = ThemesContract.ThemesColumns.PKG_NAME + "= ?";
+ String[] selectionArgs = new String[] { pkgName };
+ String[] projection = null;
+ switch (id) {
+ case LOADER_ID_ALL:
+ if (mThemeVersion >= 3) {
+ // Load all default component previews (component_id == 0)
+ selection += " AND " + PreviewColumns.COMPONENT_ID + "=?";
+ selectionArgs = new String[] { pkgName, String.valueOf(DEFAULT_COMPONENT_ID) };
+ } else {
+ // SQL query will fail if we ask for PreviewColumns.COMPONENT_ID, don't add it.
+ selectionArgs = new String[]{pkgName};
+ }
+ projection = new String[] {
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.TITLE,
+ ThemesColumns.AUTHOR,
+ ThemesColumns.WALLPAPER_URI,
+ ThemesColumns.HOMESCREEN_URI,
+ ThemesColumns.TARGET_API,
+ // Theme abilities
+ ThemesColumns.MODIFIES_LAUNCHER,
+ ThemesColumns.MODIFIES_LOCKSCREEN,
+ ThemesColumns.MODIFIES_ALARMS,
+ ThemesColumns.MODIFIES_BOOT_ANIM,
+ ThemesColumns.MODIFIES_FONTS,
+ ThemesColumns.MODIFIES_ICONS,
+ ThemesColumns.MODIFIES_NAVIGATION_BAR,
+ ThemesColumns.MODIFIES_OVERLAYS,
+ ThemesColumns.MODIFIES_RINGTONES,
+ ThemesColumns.MODIFIES_STATUS_BAR,
+ ThemesColumns.MODIFIES_NOTIFICATIONS,
+ //Previews
+ PreviewColumns.WALLPAPER_PREVIEW,
+ PreviewColumns.STATUSBAR_BACKGROUND,
+ PreviewColumns.STATUSBAR_WIFI_ICON,
+ PreviewColumns.STATUSBAR_WIFI_COMBO_MARGIN_END,
+ PreviewColumns.STATUSBAR_BLUETOOTH_ICON,
+ PreviewColumns.STATUSBAR_SIGNAL_ICON,
+ PreviewColumns.STATUSBAR_CLOCK_TEXT_COLOR,
+ PreviewColumns.STATUSBAR_BATTERY_CIRCLE,
+ PreviewColumns.STATUSBAR_BATTERY_LANDSCAPE,
+ PreviewColumns.STATUSBAR_BATTERY_PORTRAIT,
+ PreviewColumns.NAVBAR_BACK_BUTTON,
+ PreviewColumns.NAVBAR_HOME_BUTTON,
+ PreviewColumns.NAVBAR_RECENT_BUTTON,
+ PreviewColumns.ICON_PREVIEW_1,
+ PreviewColumns.ICON_PREVIEW_2,
+ PreviewColumns.ICON_PREVIEW_3,
+ PreviewColumns.LOCK_WALLPAPER_PREVIEW,
+ PreviewColumns.STYLE_PREVIEW,
+ PreviewColumns.LIVE_LOCK_SCREEN_PREVIEW
+ };
+ break;
+ case LOADER_ID_STATUS_BAR:
+ projection = new String[] {
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.TITLE,
+ PreviewColumns.STATUSBAR_BACKGROUND,
+ PreviewColumns.STATUSBAR_WIFI_ICON,
+ PreviewColumns.STATUSBAR_WIFI_COMBO_MARGIN_END,
+ PreviewColumns.STATUSBAR_BLUETOOTH_ICON,
+ PreviewColumns.STATUSBAR_SIGNAL_ICON,
+ PreviewColumns.STATUSBAR_CLOCK_TEXT_COLOR,
+ PreviewColumns.STATUSBAR_BATTERY_CIRCLE,
+ PreviewColumns.STATUSBAR_BATTERY_LANDSCAPE,
+ PreviewColumns.STATUSBAR_BATTERY_PORTRAIT
+ };
+ break;
+ case LOADER_ID_FONT:
+ uri = ThemesColumns.CONTENT_URI;
+ projection = new String[] {
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.TITLE
+ };
+ break;
+ case LOADER_ID_ICONS:
+ projection = new String[] {
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.TITLE,
+ PreviewColumns.ICON_PREVIEW_1,
+ PreviewColumns.ICON_PREVIEW_2,
+ PreviewColumns.ICON_PREVIEW_3,
+ };
+ break;
+ case LOADER_ID_WALLPAPER:
+ if (mThemeVersion >= 3) {
+ uri = PreviewColumns.COMPONENTS_URI;
+ // Load specified wallpaper previews (component_id is specified)
+ selection += " AND " + PreviewColumns.COMPONENT_ID + "=?";
+ selectionArgs = new String[]{pkgName, String.valueOf(componentId)};
+ projection = new String[]{
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.TITLE,
+ PreviewColumns.WALLPAPER_PREVIEW,
+ PreviewColumns.COMPONENT_ID
+ };
+ } else {
+ projection = new String[]{
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.TITLE,
+ PreviewColumns.WALLPAPER_PREVIEW
+ };
+ }
+ break;
+ case LOADER_ID_NAVIGATION_BAR:
+ projection = new String[] {
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.TITLE,
+ PreviewColumns.STATUSBAR_BACKGROUND,
+ PreviewColumns.NAVBAR_BACK_BUTTON,
+ PreviewColumns.NAVBAR_HOME_BUTTON,
+ PreviewColumns.NAVBAR_RECENT_BUTTON
+ };
+ break;
+ case LOADER_ID_LOCKSCREEN:
+ projection = new String[]{
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.TITLE,
+ PreviewColumns.LOCK_WALLPAPER_PREVIEW,
+ };
+ break;
+ case LOADER_ID_LIVE_LOCK_SCREEN:
+ projection = new String[]{
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.TITLE,
+ ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN,
+ PreviewColumns.LIVE_LOCK_SCREEN_PREVIEW
+ };
+ break;
+ case LOADER_ID_STYLE:
+ projection = new String[] {
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.TITLE,
+ PreviewColumns.STYLE_PREVIEW
+ };
+ break;
+ case LOADER_ID_BOOT_ANIMATION:
+ projection = new String[] {
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.TITLE
+ };
+ break;
+ case LOADER_ID_RINGTONE:
+ case LOADER_ID_NOTIFICATION:
+ case LOADER_ID_ALARM:
+ projection = new String[] {
+ ThemesColumns.PKG_NAME,
+ ThemesColumns.TITLE
+ };
+ break;
+ }
+ return new CursorLoader(context, uri, projection, selection, selectionArgs, null);
+ }
+
+ public static Object[] getRowFromCursor(Cursor cursor) {
+ Object[] row = null;
+ if (cursor != null) {
+ int colCount = cursor.getColumnCount();
+ row = new Object[colCount];
+ for (int indx = 0; indx < colCount; indx++) {
+ row[indx] = getFieldValueFromRow(cursor, indx);
+ }
+ }
+ return row;
+ }
+
+ public static Object getFieldValueFromRow(Cursor cursor, int position) {
+ switch (cursor.getType(position)) {
+ case Cursor.FIELD_TYPE_BLOB: return cursor.getBlob(position);
+ case Cursor.FIELD_TYPE_FLOAT: return cursor.getFloat(position);
+ case Cursor.FIELD_TYPE_INTEGER: return cursor.getInt(position);
+ case Cursor.FIELD_TYPE_STRING: return cursor.getString(position);
+ case Cursor.FIELD_TYPE_NULL:
+ default:
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/org/cyanogenmod/theme/util/FontConfigParser.java b/src/org/cyanogenmod/theme/util/FontConfigParser.java
new file mode 100644
index 0000000..a5b4de9
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/FontConfigParser.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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..3f5b897
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/IconPreviewHelper.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.ComposedIconInfo;
+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.PackageItemInfo;
+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;
+import android.util.SparseArray;
+
+/**
+ * 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;
+ private IconPackHelper mIconPackHelper;
+ private int mIconSize;
+
+ /**
+ * @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;
+ mIconPackHelper = new IconPackHelper(mContext);
+ try {
+ mIconPackHelper.loadIconPack(mThemePkgName);
+ } catch (NameNotFoundException e) {}
+ mIconSize = (int) (am.getLauncherLargeIconSize() * ICON_SCALE_FACTOR);
+ }
+
+ /**
+ * 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 = getDefaultIcon(packageName, activityName);
+ }
+ if (icon != null) {
+ icon.setBounds(0, 0, mIconSize, mIconSize);
+ }
+ return icon;
+ }
+
+ private Drawable getThemedIcon(String pkgName, String activityName) {
+ Drawable drawable = null;
+ ActivityInfo info = new ActivityInfo();
+ info.packageName = pkgName;
+ info.name = activityName;
+ drawable = mIconPackHelper.getDrawableForActivityWithDensity(info, mIconDpi);
+
+ return drawable;
+ }
+
+ /**
+ * Returns the default icon. This can be the normal icon associated with the app or a composed
+ * icon if the icon pack supports background, mask, and/or foreground.
+ * @param pkgName
+ * @param activityName
+ * @return
+ */
+ public Drawable getDefaultIcon(String pkgName, String activityName) {
+ Drawable drawable = null;
+ ComponentName component = new ComponentName(pkgName, activityName);
+ PackageManager pm = mContext.getPackageManager();
+ Resources res = null;
+ try {
+ ActivityInfo info = pm.getActivityInfo(component, 0);
+ ApplicationInfo appInfo = pm.getApplicationInfo(pkgName, 0);
+
+ AssetManager assets = new AssetManager();
+ assets.addAssetPath(appInfo.publicSourceDir);
+ res = new Resources(assets, mDisplayMetrics, mConfiguration);
+
+ final int iconId = info.icon != 0 ? info.icon : appInfo.icon;
+ info.themedIcon = 0;
+ setupComposedIcon(res, info, iconId);
+ drawable = getFullResIcon(res, iconId);
+ } catch (NameNotFoundException e2) {
+ Log.w(TAG, "Unable to get the icon for " + pkgName + " using default");
+ }
+ drawable = (drawable != null) ?
+ getComposedIcon(res, drawable) : getFullResDefaultActivityIcon();
+ return drawable;
+ }
+
+ private Drawable getComposedIcon(Resources res, Drawable baseIcon) {
+ ComposedIconInfo iconInfo = mIconPackHelper.getComposedIconInfo();
+ if (res != null && iconInfo != null && (iconInfo.iconBacks != null ||
+ iconInfo.iconMask != 0 || iconInfo.iconUpon != 0)) {
+ return IconPackHelper.IconCustomizer.getComposedIconDrawable(baseIcon, res, iconInfo);
+ }
+ return baseIcon;
+ }
+
+ private void setupComposedIcon(Resources res, ActivityInfo info, int iconId) {
+ ComposedIconInfo iconInfo = mIconPackHelper.getComposedIconInfo();
+ if (iconInfo.iconBacks == null && iconInfo.iconMask == 0 && iconInfo.iconUpon == 0) {
+ return;
+ }
+
+ res.setComposedIconInfo(iconInfo);
+
+ SparseArray<PackageItemInfo> icons = new SparseArray<PackageItemInfo>(1);
+ info.themedIcon = 0;
+ icons.put(iconId, info);
+ res.setIconResources(icons);
+ }
+
+ private Drawable getFullResIcon(Resources resources, int iconId) {
+ Drawable d;
+ try {
+ d = resources.getDrawableForDensity(iconId, mIconDpi, null, false);
+ } 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..ebbdb66
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/NotificationHelper.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.content.res.Resources;
+import android.graphics.BitmapFactory;
+import android.text.TextUtils;
+
+import org.cyanogenmod.theme.chooser.ChooserActivity;
+import org.cyanogenmod.theme.chooser.R;
+
+public class NotificationHelper {
+ private static final int NOTIFICATION_ID = 0x434D5443;
+
+ public static void postThemeInstalledNotification(Context context, String pkgName) {
+ String themeName = null;
+ try {
+ PackageInfo pi = context.getPackageManager().getPackageInfo(pkgName, 0);
+ if (pi.themeInfo != null) {
+ themeName = pi.themeInfo.name;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ return;
+ }
+ if (TextUtils.isEmpty(themeName)) {
+ return;
+ }
+
+ int themeCount = PreferenceUtils.getNewlyInstalledThemeCount(context) + 1;
+
+ Intent intent = new Intent(context, ChooserActivity.class);
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.putExtra("pkgName", pkgName);
+ PendingIntent pi = PendingIntent.getActivity(context, 0, intent,
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT);
+
+ String title = null;
+ String content = null;
+ final Resources res = context.getResources();
+ if (themeCount == 1) {
+ title = String.format(res.getString(
+ R.string.theme_installed_notification_title), themeName);
+ content = res.getString(R.string.theme_installed_notification_text);
+ } else {
+ title = String.format(res.getString(R.string.themes_installed_notification_title),
+ themeCount);
+ content = String.format(res.getQuantityString(
+ R.plurals.themes_installed_notification_text, themeCount -1),
+ themeName, themeCount - 1);
+ }
+ NotificationManager nm =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ Notification notice = new Notification.Builder(context)
+ .setAutoCancel(true)
+ .setOngoing(false)
+ .setContentTitle(title)
+ .setContentText(content)
+ .setContentIntent(pi)
+ .setSmallIcon(R.drawable.ic_notify)
+ .setWhen(System.currentTimeMillis())
+ .build();
+ if (themeCount > 1) notice.number = themeCount;
+ nm.notify(NOTIFICATION_ID, notice);
+ PreferenceUtils.setNewlyInstalledThemeCount(context, themeCount);
+ }
+
+ public static void cancelNotifications(Context context) {
+ NotificationManager nm = (NotificationManager)
+ context.getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancel(NOTIFICATION_ID);
+ PreferenceUtils.setNewlyInstalledThemeCount(context, 0);
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/PreferenceUtils.java b/src/org/cyanogenmod/theme/util/PreferenceUtils.java
new file mode 100644
index 0000000..080615f
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/PreferenceUtils.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.SharedPreferences;
+import android.content.pm.ThemeUtils;
+import android.content.res.Resources;
+import android.content.res.ThemeConfig;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class PreferenceUtils {
+ private static final String TAG = PreferenceUtils.class.getSimpleName();
+
+ public static final String PREF_APPLIED_BASE_THEME = "applied_base_theme";
+ public static final String PREF_UPDATED_THEMES = "updated_themes";
+ public static final String PREF_NEWLY_INSTALLED_THEME_COUNT = "newly_installed_theme_count";
+ public static final String PREF_INSTALLED_THEMES_PROCESSING = "installed_themes_processing";
+ public static final String PREF_SHOW_PER_APP_THEMING_NEW_TAG = "show_per_app_new_tag";
+
+ public static SharedPreferences getSharedPreferences(Context context) {
+ if (context == null) return null;
+ return context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
+ }
+
+ public static String getAppliedBaseTheme(Context context) {
+ SharedPreferences prefs = getSharedPreferences(context);
+ if (prefs == null) return null;
+
+ final Resources res = context.getResources();
+ final ThemeConfig config = res.getConfiguration().themeConfig;
+ String appliedTheme = config != null
+ ? config.getOverlayPkgName()
+ : ThemeConfig.SYSTEM_DEFAULT;
+ return prefs.getString(PREF_APPLIED_BASE_THEME, appliedTheme);
+ }
+
+ public static void setAppliedBaseTheme(Context context, String pkgName) {
+ SharedPreferences prefs = getSharedPreferences(context);
+ if (prefs != null) {
+ prefs.edit().putString(PREF_APPLIED_BASE_THEME, pkgName).apply();
+ }
+ }
+
+ public static Set<String> getUpdatedThemes(Context context) {
+ SharedPreferences prefs = getSharedPreferences(context);
+ if (prefs == null) return null;
+
+ return prefs.getStringSet(PREF_UPDATED_THEMES, null);
+ }
+
+ public static void addUpdatedTheme(Context context, String pkgName) {
+ SharedPreferences prefs = getSharedPreferences(context);
+ if (prefs != null) {
+ Set<String> updatedThemes = new HashSet<String>();
+ Set<String> current = prefs.getStringSet(PREF_UPDATED_THEMES, null);
+ if (current != null) {
+ updatedThemes.addAll(current);
+ }
+ if (updatedThemes.add(pkgName)) {
+ prefs.edit().putStringSet(PREF_UPDATED_THEMES, updatedThemes).apply();
+ }
+ }
+ }
+
+ public static void removeUpdatedTheme(Context context, String pkgName) {
+ SharedPreferences prefs = getSharedPreferences(context);
+ if (prefs != null) {
+ Set<String> updatedThemes = new HashSet<String>();
+ Set<String> current = prefs.getStringSet(PREF_UPDATED_THEMES, null);
+ if (current != null) {
+ updatedThemes.addAll(current);
+ }
+ if (updatedThemes.remove(pkgName)) {
+ prefs.edit().putStringSet(PREF_UPDATED_THEMES, updatedThemes).apply();
+ }
+ }
+ }
+
+ public static boolean hasThemeBeenUpdated(Context context, String pkgName) {
+ Set<String> updatedThemes = getUpdatedThemes(context);
+ return updatedThemes != null && updatedThemes.contains(pkgName);
+ }
+
+ public static int getNewlyInstalledThemeCount(Context context) {
+ SharedPreferences prefs = getSharedPreferences(context);
+ if (prefs == null) return 0;
+
+ return prefs.getInt(PREF_NEWLY_INSTALLED_THEME_COUNT, 0);
+ }
+
+ public static void setNewlyInstalledThemeCount(Context context, int count) {
+ SharedPreferences prefs = getSharedPreferences(context);
+ if (prefs != null) {
+ prefs.edit().putInt(PREF_NEWLY_INSTALLED_THEME_COUNT, count).apply();
+ }
+ }
+
+ public static boolean getShowPerAppThemeNewTag(Context context) {
+ SharedPreferences prefs = getSharedPreferences(context);
+ if (prefs != null) {
+ return prefs.getBoolean(PREF_SHOW_PER_APP_THEMING_NEW_TAG, true);
+ }
+
+ return false;
+ }
+
+ public static void setShowPerAppThemeNewTag(Context context, boolean show) {
+ SharedPreferences prefs = getSharedPreferences(context);
+ if (prefs != null) {
+ prefs.edit().putBoolean(PREF_SHOW_PER_APP_THEMING_NEW_TAG, show).apply();
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/ThemedTypefaceHelper.java b/src/org/cyanogenmod/theme/util/ThemedTypefaceHelper.java
new file mode 100644
index 0000000..cf5ddfc
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/ThemedTypefaceHelper.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.graphics.FontListParser;
+import android.graphics.FontListParser.Family;
+import android.graphics.Typeface;
+import android.util.Log;
+
+import org.cyanogenmod.internal.util.ThemeUtils;
+
+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<FontListParser.Family> mFamilies;
+ private Typeface[] mTypefaces = new Typeface[4];
+
+ public void load(Context context, String pkgName) {
+ try {
+ loadThemedFonts(context, pkgName);
+ return;
+ } catch(Exception e) {
+ Log.w(TAG, "Unable to parse and load themed fonts for " + pkgName +
+ ". 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", e);
+ }
+
+ // 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 + File.separator + ThemeUtils.FONT_XML);
+ FontListParser.Config fontConfig = FontListParser.parse(is, FONTS_DIR);
+ mFamilies = fontConfig.families;
+
+ //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);
+ FontListParser.Config fontConfig = FontListParser.parse(is, SYSTEM_FONTS_DIR);
+ mFamilies = fontConfig.families;
+
+ //Load the typefaces for sans-serif
+ Family sanSerif = getFamily(FAMILY_SANS_SERIF);
+ if (mTypefaces[Typeface.NORMAL] == null) {
+ mTypefaces[Typeface.NORMAL] = loadSystemTypeface(sanSerif, Typeface.NORMAL);
+ }
+ if (mTypefaces[Typeface.BOLD] == null) {
+ mTypefaces[Typeface.BOLD] = loadSystemTypeface(sanSerif, Typeface.BOLD);
+ }
+ if (mTypefaces[Typeface.ITALIC] == null) {
+ mTypefaces[Typeface.ITALIC] = loadSystemTypeface(sanSerif, Typeface.ITALIC);
+ }
+ if (mTypefaces[Typeface.BOLD_ITALIC] == null) {
+ mTypefaces[Typeface.BOLD_ITALIC] = loadSystemTypeface(sanSerif, Typeface.BOLD_ITALIC);
+ }
+ 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.name.equals(familyName)) {
+ return family;
+ }
+ }
+ throw new Exception("Unable to find " + familyName);
+ }
+
+ private Typeface loadTypeface(Family family, int style) {
+ AssetManager assets = mThemeContext.getAssets();
+ String path = family.fonts.get(style).fontName;
+ return Typeface.createFromAsset(assets, path);
+ }
+
+ private Typeface loadSystemTypeface(Family family, int style) {
+ return Typeface.createFromFile(family.fonts.get(style).fontName);
+ }
+
+ 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/TypefaceHelperCache.java b/src/org/cyanogenmod/theme/util/TypefaceHelperCache.java
new file mode 100644
index 0000000..76183f8
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/TypefaceHelperCache.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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 java.util.HashMap;
+import java.util.Map;
+
+public class TypefaceHelperCache {
+ private static TypefaceHelperCache sHelperCache;
+ private final Map<String, ThemedTypefaceHelper> mCache;
+
+ private TypefaceHelperCache() {
+ mCache = new HashMap<String, ThemedTypefaceHelper>();
+ }
+
+ public static synchronized TypefaceHelperCache getInstance() {
+ if (sHelperCache == null) {
+ sHelperCache = new TypefaceHelperCache();
+ }
+ return sHelperCache;
+ }
+
+ public ThemedTypefaceHelper getHelperForTheme(Context context, String pkgName) {
+ synchronized (mCache) {
+ ThemedTypefaceHelper helper = mCache.get(pkgName);
+ if (helper == null) {
+ helper = new ThemedTypefaceHelper();
+ helper.load(context, pkgName);
+ mCache.put(pkgName, helper);
+ }
+ return helper;
+ }
+ }
+
+ public int getTypefaceCount() {
+ synchronized (mCache) {
+ return mCache.size();
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/Utils.java b/src/org/cyanogenmod/theme/util/Utils.java
new file mode 100644
index 0000000..94afecb
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/Utils.java
@@ -0,0 +1,724 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.WallpaperManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PermissionInfo;
+import android.content.pm.ResolveInfo;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.ThemeConfig;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.IWindowManager;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+
+import org.cyanogenmod.theme.chooser.ChooserActivity;
+
+import cyanogenmod.externalviews.KeyguardExternalView;
+import cyanogenmod.providers.CMSettings;
+import cyanogenmod.providers.ThemesContract;
+
+import org.cyanogenmod.internal.util.ThemeUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.security.InvalidParameterException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import static android.content.res.ThemeConfig.SYSTEM_DEFAULT;
+
+public class Utils {
+ private static final String TAG = Utils.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private static final String OVERLAY_BASE_PATH = "overlays" + File.separator;
+
+ 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();
+ }
+ }
+ }
+
+ public static boolean hasNavigationBar(Context context) {
+ boolean needsNavigationBar = false;
+ try {
+ IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+ needsNavigationBar = wm.needsNavigationBar();
+ } catch (RemoteException e) {
+ }
+ // Need to also check for devices with hardware keys where the user has chosen to use
+ // the on screen navigation bar
+ needsNavigationBar = needsNavigationBar ||
+ CMSettings.Secure.getInt(context.getContentResolver(),
+ CMSettings.Secure.DEV_FORCE_SHOW_NAVBAR, 0) == 1;
+ return needsNavigationBar;
+ }
+
+ public static Bitmap loadBitmapBlob(Cursor cursor, int columnIdx) {
+ if (columnIdx < 0) {
+ Log.w(TAG, "loadBitmapBlob(): Invalid index provided, returning null");
+ return null;
+ }
+
+ if (cursor.getType(columnIdx) == Cursor.FIELD_TYPE_STRING) {
+ return loadBitmapFile(cursor, columnIdx);
+ }
+
+ byte[] blob = cursor.getBlob(columnIdx);
+ if (blob == null) return null;
+ return BitmapFactory.decodeByteArray(blob, 0, blob.length);
+ }
+
+ public static Bitmap loadBitmapFile(Cursor cursor, int columnIdx) {
+ if (columnIdx < 0) {
+ Log.w(TAG, "loadBitmapFile(): Invalid index provided, returning null");
+ return null;
+ }
+ String path = cursor.getString(columnIdx);
+ if (TextUtils.isEmpty(path)) {
+ return null;
+ }
+
+ Bitmap image = null;
+ FileInputStream inputStream;
+ try {
+ inputStream = new FileInputStream(path);
+ image = BitmapFactory.decodeStream(inputStream);
+ inputStream.close();
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to open preview " + path, e);
+ }
+
+ return image;
+ }
+
+ public static String getBatteryIndex(int type) {
+ switch(type) {
+ case 2:
+ return ThemesContract.PreviewColumns.STATUSBAR_BATTERY_CIRCLE;
+ case 5:
+ return ThemesContract.PreviewColumns.STATUSBAR_BATTERY_LANDSCAPE;
+ default:
+ return ThemesContract.PreviewColumns.STATUSBAR_BATTERY_PORTRAIT;
+ }
+ }
+
+ public static Bitmap getRegularWallpaperBitmap(Context context) {
+ WallpaperManager wm = WallpaperManager.getInstance(context);
+
+ Bitmap bitmap = null;
+ // desktop wallpaper here
+ Bitmap wallpaper = wm.getBitmap();
+ if (wallpaper == null) {
+ return null;
+ }
+
+ Point size = new Point();
+ WindowManager windowManager =
+ (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ windowManager.getDefaultDisplay().getRealSize(size);
+
+ final int dw = size.x;
+ final int dh = size.y;
+
+ // Center the scaled image
+ float scale = Math.max(1f, Math.max(dw / (float) wallpaper.getWidth(),
+ dh / (float) wallpaper.getHeight()));
+
+ final int scaledWidth = Math.round((wallpaper.getWidth() * scale));
+ final int scaledHeight = Math.round((wallpaper.getHeight() * scale));
+
+ // TODO: set xOffset to wm.getLastWallpaperX() once available
+ int xOffset = wm.getLastWallpaperX();
+ // x offset
+ if (xOffset == -1) {
+ xOffset = 0;
+ } else {
+ xOffset *= -1;
+ }
+
+ // y offsets
+ // TODO: set yOffset to wm.getLastWallpaperY() once available
+ int yOffset = wm.getLastWallpaperY();
+ if (yOffset == -1) {
+ yOffset = 0;
+ } else {
+ yOffset *= -1;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "scale: " + scale);
+ Log.d(TAG, "scaledWidth: " + scaledWidth);
+ Log.d(TAG, "scaledHeight: " + scaledHeight);
+ Log.d(TAG, "wallpaper size: width: " + wallpaper.getWidth() +
+ ", height: " + wallpaper.getHeight());
+ Log.d(TAG, "xOffset: " + xOffset);
+ Log.d(TAG, "yOffset: " + yOffset);
+ }
+
+ try {
+ if (wallpaper.getHeight() < dh) {
+ // need to scale it up vertically
+
+ if (wallpaper.getHeight() > wallpaper.getWidth()) {
+ // handle portrait wallpaper
+ float diff = scaledWidth - dw;
+ int diffhalf = Math.round(diff / 2);
+
+ bitmap = Bitmap.createScaledBitmap(wallpaper, scaledWidth, scaledHeight, true);
+ bitmap = Bitmap.createBitmap(bitmap, diffhalf, 0, dw, dh);
+ bitmap = Bitmap.createBitmap(bitmap, xOffset, 0, dw, dh);
+ } else {
+ int goldenWidth = Math.round(wallpaper.getHeight() * 1.125f);
+ int spaceA = (wallpaper.getWidth() - goldenWidth) / 2;
+ int spaceB = (goldenWidth - Math.round(dh / scale)) / 2;
+
+ bitmap = Bitmap.createBitmap(wallpaper, spaceA, 0, goldenWidth,
+ wallpaper.getHeight());
+ int left = spaceB + Math.round(xOffset / scale);
+ bitmap = Bitmap.createBitmap(bitmap, left, 0, Math.round(dw / scale),
+ Math.round(dh / scale));
+ }
+
+ } else if (wallpaper.getWidth() < dw) {
+ // need to scale it up horizontally
+
+ if (wallpaper.getHeight() > wallpaper.getWidth()) {
+ // handle portrait wallpaper
+ return wallpaper;
+
+ } else {
+ // handle landscape wallpaper
+ float diff = wallpaper.getHeight() - wallpaper.getWidth();
+ int diffhalf = Math.round(diff / 2);
+
+ if (diffhalf < 0) {
+ return wallpaper;
+ }
+
+ bitmap = Bitmap.createBitmap(
+ wallpaper, diffhalf, 0,
+ wallpaper.getWidth(), wallpaper.getWidth());
+
+ // blow it up
+ bitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledWidth, true);
+
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0, dw, dh);
+ }
+
+ } else {
+ // sometimes the wallpaper manager gives incorrect offsets,
+ // and adds like 200 pixels randomly. If it's bigger than we can handle, calculate
+ // our own :)
+ if (yOffset + dh > wallpaper.getHeight()) {
+ yOffset = (wallpaper.getHeight() - dh) / 2;
+ }
+ if (xOffset + dw > wallpaper.getWidth()) {
+ yOffset = (wallpaper.getWidth() - dw) / 2;
+ }
+ bitmap = Bitmap.createBitmap(wallpaper, xOffset, yOffset, dw, dh);
+ }
+ } catch (IllegalArgumentException e) {
+ // Cropping/resizing failed so return the original
+ bitmap = wallpaper;
+ }
+ return bitmap;
+ }
+
+ public static boolean isRecentTaskHome(Context context) {
+ final ActivityManager am =
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+ final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasks(
+ 2, ActivityManager.RECENT_IGNORE_UNAVAILABLE);
+ if (recentTasks.size() > 1) {
+ ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(1);
+
+ Intent intent = new Intent(recentInfo.baseIntent);
+ if (recentInfo.origActivity != null) {
+ intent.setComponent(recentInfo.origActivity);
+ }
+
+ // Now check if this recent task is a launcher
+ if (isCurrentHomeActivity(context, intent.getComponent())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean isRecentTaskThemeStore(Context context) {
+ final ActivityManager am =
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+ final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasks(
+ 2, ActivityManager.RECENT_IGNORE_UNAVAILABLE);
+ if (recentTasks.size() > 0) {
+ ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(0);
+
+ Intent intent = new Intent(recentInfo.baseIntent);
+ if (recentInfo.origActivity != null) {
+ intent.setComponent(recentInfo.origActivity);
+ }
+
+ if (intent.getComponent()
+ .getPackageName().equals(ChooserActivity.THEME_STORE_PACKAGE)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ public static String getTopTaskPackageName(Context context) {
+ final ActivityManager am =
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasks(1, 0);
+ if (recentTasks.size() > 0) {
+ ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(0);
+ if (recentInfo.origActivity != null) {
+ return recentInfo.origActivity.getPackageName();
+ }
+ if (recentInfo.baseIntent != null) {
+ return recentInfo.baseIntent.getComponent().getPackageName();
+ }
+ }
+ return null;
+ }
+
+ public static boolean hasPerAppThemesApplied(Context context) {
+ final Configuration config = context.getResources().getConfiguration();
+ final ThemeConfig themeConfig = config != null ? config.themeConfig : null;
+ if (themeConfig != null) {
+ Map<String, ThemeConfig.AppTheme> themes = themeConfig.getAppThemes();
+ for (String appPkgName : themes.keySet()) {
+ if (ThemeUtils.isPerAppThemeComponent(appPkgName)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Method to identify if a theme explicitly overlays a particular app. Explicit is defined
+ * as having files in overlays/appPkgName/
+ * @param context
+ * @param appPkgNane
+ * @param themePkgName
+ * @return
+ */
+ public static boolean themeHasOverlayForApp(Context context, String appPkgNane,
+ String themePkgName) {
+ boolean hasExplicitOverlay = false;
+ if (ThemeConfig.SYSTEM_DEFAULT.equals(themePkgName)) {
+ hasExplicitOverlay = true;
+ } else {
+ try {
+ Context themeContext = context.createPackageContext(themePkgName, 0);
+ if (themeContext != null) {
+ AssetManager assets = themeContext.getAssets();
+ String[] files = assets.list(OVERLAY_BASE_PATH + appPkgNane);
+ if (files != null && files.length > 0) hasExplicitOverlay = true;
+ }
+ } catch (Exception e) {
+ // don't care, we'll return false and let the caller handle things
+ }
+ }
+ return hasExplicitOverlay;
+ }
+
+ private static boolean isCurrentHomeActivity(Context context,
+ ComponentName component) {
+ final PackageManager pm = context.getPackageManager();
+ ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
+ .resolveActivityInfo(pm, 0);
+
+ return homeInfo != null
+ && homeInfo.packageName.equals(component.getPackageName())
+ && homeInfo.name.equals(component.getClassName());
+ }
+
+ /**
+ * Returns the resource-IDs for all attributes specified in the given
+ * <declare-styleable>-resource tag as an int array.
+ * stackoverflow.com/questions/13816596/accessing-declare-styleable-resources-programatically
+ *
+ * @param name
+ * @return
+ */
+ public static final int[] getResourceDeclareStyleableIntArray(String pkgName, String name) {
+ try {
+ //use reflection to access the resource class
+ Field[] fields2 =
+ Class.forName(pkgName + ".R$styleable").getFields();
+
+ //browse all fields
+ for (Field f : fields2) {
+ //pick matching field
+ if (f.getName().equals(name)) {
+ //return as int array
+ int[] ret = (int[])f.get(null);
+ return ret;
+ }
+ }
+ }
+ catch (Throwable t) {
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieves the list of dangerous permissions not granted to the supplied package. This method
+ * is not capable of identifying if a given permission was previously revoked by the user or
+ * if the user decided not to be asked again.
+ *
+ * @param context
+ * @param pkgName
+ * @return Returns an array of Strings with the name of the permissions. An empty array will
+ * be returned if all dangerous permissions have been already granted.
+ */
+ public static String[] getDangerousPermissionsNotGranted(Context context, String pkgName)
+ throws InvalidParameterException {
+ LinkedList<String> permissionsNotGranted = new LinkedList<String>();
+ PackageInfo pkgInfo = null;
+ PackageManager pm = context.getPackageManager();
+ try {
+ pkgInfo = pm.getPackageInfo(pkgName, PackageManager.GET_PERMISSIONS);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new InvalidParameterException("Package " + pkgName + " not found");
+ }
+
+ String[] requestedPermissions = pkgInfo.requestedPermissions;
+ int[] requestedPermissionsFlags = pkgInfo.requestedPermissionsFlags;
+
+ for (int indx = 0; indx < requestedPermissions.length; indx++) {
+ if ((requestedPermissionsFlags[indx] & PackageInfo.REQUESTED_PERMISSION_GRANTED) == 0) {
+ try {
+ PermissionInfo pi = pm.getPermissionInfo(requestedPermissions[indx],0);
+
+ if (pi.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS) {
+ permissionsNotGranted.add(requestedPermissions[indx]);
+ if (DEBUG) {
+ Log.d(TAG, "Permission " + requestedPermissions[indx] + "not granted");
+ }
+ }
+ } catch(PackageManager.NameNotFoundException e) {
+ //If package manager doesn't know of the permission we just continue since
+ //this permission won't end up in the list
+ }
+ }
+ }
+ return permissionsNotGranted.toArray(new String[permissionsNotGranted.size()]);
+ }
+
+ /**
+ * Builds a ComponentName to identify the activity associated with the
+ * KeyguardExternalView.CATEGORY_KEYGUARD_GRANT_PERMISSION category in the given package
+ * @param context
+ * @param packageName
+ * @return Returns the ComponentName or null if no activity was found.
+ */
+ private static ComponentName getPermissionGranterComponentName(Context context,
+ String packageName) {
+ Intent rIntent = new Intent()
+ .setPackage(packageName)
+ .addCategory(KeyguardExternalView.CATEGORY_KEYGUARD_GRANT_PERMISSION);
+
+ List<ResolveInfo> resolveInfo = context.getPackageManager().
+ queryIntentActivities(rIntent, PackageManager.GET_RESOLVED_FILTER);
+
+ if (resolveInfo.size() >= 1) {
+ if (DEBUG) {
+ if (resolveInfo.size() >= 2) {
+ Log.w(TAG, "Got " + resolveInfo.size() + " resolvers! Defaulting to "
+ + resolveInfo.get(0).activityInfo.name);
+ }
+ }
+ }
+ return (resolveInfo.size() >=1 ) ?
+ new ComponentName(packageName, resolveInfo.get(0).activityInfo.name) :
+ null;
+ }
+
+ /**
+ * Builds an intent used to request the user to grant or revoke the supplied permissions.
+ * The intent will set the KeyguardExternalView.CATEGORY_KEYGUARD_GRANT_PERMISSION
+ * category and an extra containing the list of permissions identified by
+ * KeyguardExternalView.EXTRA_PERMISSION_LIST
+ * @param context
+ * @param packageName
+ * @param permissionList
+ * @return Returns the intent if an activity associated with
+ * KeyguardExternalView.CATEGORY_KEYGUARD_GRANT_PERMISSION category was found. Otherwise, it
+ * returns null
+ */
+ public static Intent buildPermissionGrantRequestIntent(Context context, String packageName,
+ String[] permissionList) {
+ ComponentName componentName = getPermissionGranterComponentName(context, packageName);
+ if (componentName == null) return null;
+
+ Intent permissionIntent = new Intent()
+ .setComponent(componentName)
+ .addCategory(KeyguardExternalView.CATEGORY_KEYGUARD_GRANT_PERMISSION)
+ .setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS|Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(KeyguardExternalView.EXTRA_PERMISSION_LIST, permissionList);
+ return permissionIntent;
+ }
+
+ public static String getDefaultThemePackageName(Context context) {
+ final String defaultThemePkg = CMSettings.Secure.getString(context.getContentResolver(),
+ CMSettings.Secure.DEFAULT_THEME_PACKAGE);
+ if (!TextUtils.isEmpty(defaultThemePkg)) {
+ PackageManager pm = context.getPackageManager();
+ try {
+ if (pm.getPackageInfo(defaultThemePkg, 0) != null) {
+ return defaultThemePkg;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // doesn't exist so system will be default
+ Log.w(TAG, "Default theme " + defaultThemePkg + " not found", e);
+ }
+ }
+
+ return SYSTEM_DEFAULT;
+ }
+}
diff --git a/src/org/cyanogenmod/theme/util/WallpaperUtils.java b/src/org/cyanogenmod/theme/util/WallpaperUtils.java
new file mode 100644
index 0000000..c07b99b
--- /dev/null
+++ b/src/org/cyanogenmod/theme/util/WallpaperUtils.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.WallpaperManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class WallpaperUtils {
+ private static final String TAG = WallpaperUtils.class.getSimpleName();
+ private static final int DEFAULT_COMPRESS_QUALITY = 90;
+
+ /**
+ * createThumbnail from WallpaperCropActivity in f/b/packages/WallpaperCropper, renamed
+ * tp createPreview.
+ */
+ public static Bitmap createPreview(Point size, Context context, Uri uri, byte[] imageBytes,
+ Resources res, int resId, int rotation, boolean leftAligned) {
+ int width = size.x;
+ int height = size.y;
+
+ BitmapCropTask cropTask;
+ if (uri != null) {
+ cropTask = new BitmapCropTask(
+ context, uri, null, rotation, width, height, false, true, null);
+ } else if (imageBytes != null) {
+ cropTask = new BitmapCropTask(
+ imageBytes, null, rotation, width, height, false, true, null);
+ } else {
+ cropTask = new BitmapCropTask(
+ context, res, resId, null, rotation, width, height, false, true, null);
+ }
+ Point bounds = cropTask.getImageBounds();
+ if (bounds == null || bounds.x == 0 || bounds.y == 0) {
+ return null;
+ }
+
+ Matrix rotateMatrix = new Matrix();
+ rotateMatrix.setRotate(rotation);
+ float[] rotatedBounds = new float[] { bounds.x, bounds.y };
+ rotateMatrix.mapPoints(rotatedBounds);
+ rotatedBounds[0] = Math.abs(rotatedBounds[0]);
+ rotatedBounds[1] = Math.abs(rotatedBounds[1]);
+
+ RectF cropRect = getMaxCropRect(
+ (int) rotatedBounds[0], (int) rotatedBounds[1], width, height, leftAligned);
+ cropTask.setCropBounds(cropRect);
+
+ if (cropTask.cropBitmap()) {
+ return cropTask.getCroppedBitmap();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * getMaxCropRect from WallpaperCropActivity in f/b/packages/WallpaperCropper
+ */
+ protected static RectF getMaxCropRect(
+ int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) {
+ RectF cropRect = new RectF();
+ // Get a crop rect that will fit this
+ if (inWidth / (float) inHeight > outWidth / (float) outHeight) {
+ cropRect.top = 0;
+ cropRect.bottom = inHeight;
+ cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2;
+ cropRect.right = inWidth - cropRect.left;
+ if (leftAligned) {
+ cropRect.right -= cropRect.left;
+ cropRect.left = 0;
+ }
+ } else {
+ cropRect.left = 0;
+ cropRect.right = inWidth;
+ cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2;
+ cropRect.bottom = inHeight - cropRect.top;
+ }
+ return cropRect;
+ }
+
+ /**
+ * convertExtensionToCompressFormat from WallpaperCropActivity in f/b/packages/WallpaperCropper
+ */
+ protected static Bitmap.CompressFormat convertExtensionToCompressFormat(String extension) {
+ return extension.equals("png") ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG;
+ }
+
+ /**
+ * getFileExtension from WallpaperCropActivity in f/b/packages/WallpaperCropper
+ */
+ protected static String getFileExtension(String requestFormat) {
+ String outputFormat = (requestFormat == null)
+ ? "jpg"
+ : requestFormat;
+ outputFormat = outputFormat.toLowerCase();
+ return (outputFormat.equals("png") || outputFormat.equals("gif"))
+ ? "png" // We don't support gif compression.
+ : "jpg";
+ }
+
+ /**
+ * BitmapCropTask from WallpaperCropActivity in f/b/packages/WallpaperCropper
+ */
+ protected static class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
+ Uri mInUri = null;
+ Context mContext;
+ String mInFilePath;
+ byte[] mInImageBytes;
+ int mInResId = 0;
+ InputStream mInStream;
+ RectF mCropBounds = null;
+ int mOutWidth, mOutHeight;
+ int mRotation;
+ String mOutputFormat = "jpg"; // for now
+ boolean mSetWallpaper;
+ boolean mSaveCroppedBitmap;
+ Bitmap mCroppedBitmap;
+ Runnable mOnEndRunnable;
+ Resources mResources;
+ OnBitmapCroppedHandler mOnBitmapCroppedHandler;
+ boolean mNoCrop;
+ boolean mImageFromAsset;
+
+ public BitmapCropTask(byte[] imageBytes,
+ RectF cropBounds, int rotation, int outWidth, int outHeight,
+ boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
+ mInImageBytes = imageBytes;
+ init(cropBounds, rotation,
+ outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
+ }
+
+ public BitmapCropTask(Context c, Uri inUri,
+ RectF cropBounds, int rotation, int outWidth, int outHeight,
+ boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
+ mContext = c;
+ mInUri = inUri;
+ init(cropBounds, rotation,
+ outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
+ }
+
+ public BitmapCropTask(Context c, Resources res, int inResId,
+ RectF cropBounds, int rotation, int outWidth, int outHeight,
+ boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
+ mContext = c;
+ mInResId = inResId;
+ mResources = res;
+ init(cropBounds, rotation,
+ outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
+ }
+
+ private void init(RectF cropBounds, int rotation, int outWidth, int outHeight,
+ boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
+ mCropBounds = cropBounds;
+ mRotation = rotation;
+ mOutWidth = outWidth;
+ mOutHeight = outHeight;
+ mSetWallpaper = setWallpaper;
+ mSaveCroppedBitmap = saveCroppedBitmap;
+ mOnEndRunnable = onEndRunnable;
+ }
+
+ // Helper to setup input stream
+ private void regenerateInputStream() {
+ if (mInUri == null && mInResId == 0 && mInFilePath == null &&
+ mInImageBytes == null && !mImageFromAsset) {
+ Log.w(TAG, "cannot read original file, no input URI, resource ID, or " +
+ "image byte array given");
+ } else {
+ Utils.closeQuiet(mInStream);
+ try {
+ if (mInUri != null) {
+ mInStream = new BufferedInputStream(
+ mContext.getContentResolver().openInputStream(mInUri));
+ } else if (mInFilePath != null) {
+ mInStream = mContext.openFileInput(mInFilePath);
+ } else if (mInImageBytes != null) {
+ mInStream = new BufferedInputStream(
+ new ByteArrayInputStream(mInImageBytes));
+ } else {
+ mInStream = new BufferedInputStream(
+ mResources.openRawResource(mInResId));
+ }
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "cannot read file: " + mInUri.toString(), e);
+ }
+ }
+ }
+
+ public Point getImageBounds() {
+ regenerateInputStream();
+ if (mInStream != null) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(mInStream, null, options);
+ if (options.outWidth != 0 && options.outHeight != 0) {
+ return new Point(options.outWidth, options.outHeight);
+ }
+ }
+ return null;
+ }
+
+ public void setCropBounds(RectF cropBounds) {
+ mCropBounds = cropBounds;
+ }
+
+ public Bitmap getCroppedBitmap() {
+ return mCroppedBitmap;
+ }
+ public boolean cropBitmap() {
+ boolean failure = false;
+
+ regenerateInputStream();
+
+ WallpaperManager wallpaperManager = null;
+ if (mSetWallpaper) {
+ wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext());
+ }
+ if (mSetWallpaper && mNoCrop && mInStream != null) {
+ try {
+ wallpaperManager.setStream(mInStream);
+ } catch (IOException e) {
+ Log.w(TAG, "cannot write stream to wallpaper", e);
+ failure = true;
+ }
+ return !failure;
+ }
+ if (mInStream != null) {
+ // Find crop bounds (scaled to original image size)
+ Rect roundedTrueCrop = new Rect();
+ Matrix rotateMatrix = new Matrix();
+ Matrix inverseRotateMatrix = new Matrix();
+ if (mRotation > 0) {
+ rotateMatrix.setRotate(mRotation);
+ inverseRotateMatrix.setRotate(-mRotation);
+
+ mCropBounds.roundOut(roundedTrueCrop);
+ mCropBounds = new RectF(roundedTrueCrop);
+
+ Point bounds = getImageBounds();
+
+ float[] rotatedBounds = new float[] { bounds.x, bounds.y };
+ rotateMatrix.mapPoints(rotatedBounds);
+ rotatedBounds[0] = Math.abs(rotatedBounds[0]);
+ rotatedBounds[1] = Math.abs(rotatedBounds[1]);
+
+ mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2);
+ inverseRotateMatrix.mapRect(mCropBounds);
+ mCropBounds.offset(bounds.x/2, bounds.y/2);
+
+ regenerateInputStream();
+ }
+
+ mCropBounds.roundOut(roundedTrueCrop);
+
+ if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
+ Log.w(TAG, "crop has bad values for full size image");
+ failure = true;
+ return false;
+ }
+
+ // See how much we're reducing the size of the image
+ int scaleDownSampleSize = Math.min(roundedTrueCrop.width() / mOutWidth,
+ roundedTrueCrop.height() / mOutHeight);
+
+ // Attempt to open a region decoder
+ BitmapRegionDecoder decoder = null;
+ try {
+ decoder = BitmapRegionDecoder.newInstance(mInStream, true);
+ } catch (IOException e) {
+ Log.w(TAG, "cannot open region decoder for file: " + mInUri.toString(), e);
+ }
+
+ Bitmap crop = null;
+ if (decoder != null) {
+ // Do region decoding to get crop bitmap
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ if (scaleDownSampleSize > 1) {
+ options.inSampleSize = scaleDownSampleSize;
+ }
+ crop = decoder.decodeRegion(roundedTrueCrop, options);
+ decoder.recycle();
+ }
+
+ if (crop == null) {
+ // BitmapRegionDecoder has failed, try to crop in-memory
+ regenerateInputStream();
+ Bitmap fullSize = null;
+ if (mInStream != null) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ if (scaleDownSampleSize > 1) {
+ options.inSampleSize = scaleDownSampleSize;
+ }
+ fullSize = BitmapFactory.decodeStream(mInStream, null, options);
+ }
+ if (fullSize != null) {
+ mCropBounds.left /= scaleDownSampleSize;
+ mCropBounds.top /= scaleDownSampleSize;
+ mCropBounds.bottom /= scaleDownSampleSize;
+ mCropBounds.right /= scaleDownSampleSize;
+ mCropBounds.roundOut(roundedTrueCrop);
+
+ crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
+ roundedTrueCrop.top, roundedTrueCrop.width(),
+ roundedTrueCrop.height());
+ }
+ }
+
+ if (crop == null) {
+ Log.w(TAG, "cannot decode file: " + mInUri.toString());
+ failure = true;
+ return false;
+ }
+ if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) {
+ float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() };
+ rotateMatrix.mapPoints(dimsAfter);
+ dimsAfter[0] = Math.abs(dimsAfter[0]);
+ dimsAfter[1] = Math.abs(dimsAfter[1]);
+
+ if (!(mOutWidth > 0 && mOutHeight > 0)) {
+ mOutWidth = Math.round(dimsAfter[0]);
+ mOutHeight = Math.round(dimsAfter[1]);
+ }
+
+ RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]);
+ RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight);
+
+ Matrix m = new Matrix();
+ if (mRotation == 0) {
+ m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
+ } else {
+ Matrix m1 = new Matrix();
+ m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f);
+ Matrix m2 = new Matrix();
+ m2.setRotate(mRotation);
+ Matrix m3 = new Matrix();
+ m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f);
+ Matrix m4 = new Matrix();
+ m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
+
+ Matrix c1 = new Matrix();
+ c1.setConcat(m2, m1);
+ Matrix c2 = new Matrix();
+ c2.setConcat(m4, m3);
+ m.setConcat(c2, c1);
+ }
+
+ Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
+ (int) returnRect.height(), Bitmap.Config.ARGB_8888);
+ if (tmp != null) {
+ Canvas c = new Canvas(tmp);
+ Paint p = new Paint();
+ p.setFilterBitmap(true);
+ c.drawBitmap(crop, m, p);
+ crop = tmp;
+ }
+ }
+
+ if (mSaveCroppedBitmap) {
+ mCroppedBitmap = crop;
+ }
+
+ // Get output compression format
+ Bitmap.CompressFormat cf =
+ convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
+
+ // Compress to byte array
+ ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
+ if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
+ // If we need to set to the wallpaper, set it
+ if (mSetWallpaper && wallpaperManager != null) {
+ try {
+ byte[] outByteArray = tmpOut.toByteArray();
+ wallpaperManager.setStream(new ByteArrayInputStream(outByteArray));
+ if (mOnBitmapCroppedHandler != null) {
+ mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "cannot write stream to wallpaper", e);
+ failure = true;
+ }
+ }
+ } else {
+ Log.w(TAG, "cannot compress bitmap");
+ failure = true;
+ }
+ }
+ return !failure; // True if any of the operations failed
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return cropBitmap();
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (mOnEndRunnable != null) {
+ mOnEndRunnable.run();
+ }
+ }
+
+ public interface OnBitmapCroppedHandler {
+ public void onBitmapCropped(byte[] imageBytes);
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/widget/AutoSnapHorizontalScrollView.java b/src/org/cyanogenmod/theme/widget/AutoSnapHorizontalScrollView.java
new file mode 100644
index 0000000..29534f7
--- /dev/null
+++ b/src/org/cyanogenmod/theme/widget/AutoSnapHorizontalScrollView.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+
+public class AutoSnapHorizontalScrollView extends HorizontalScrollView {
+ private static final int SNAP_ON_UP_DELAY = 250;
+
+ private int mScrollPositionOnUp;
+
+ enum EventStates {
+ SCROLLING,
+ FLING
+ }
+
+ private EventStates mSystemState = EventStates.SCROLLING;
+
+ public AutoSnapHorizontalScrollView(Context context) {
+ super(context);
+ }
+
+ public AutoSnapHorizontalScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AutoSnapHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ Runnable mSnapRunnable = new Runnable(){
+ @Override
+ public void run() {
+ snapItems();
+ mSystemState = EventStates.SCROLLING;
+ }
+ };
+
+ /**
+ * Added runnable for snapping on an item when the user lifts up their finger and
+ * there is no scrolling taking place (i.e. no flinging)
+ */
+ Runnable mSnapOnUpRunnable = new Runnable(){
+ @Override
+ public void run() {
+ int scrollX = getScrollX();
+ if (scrollX != mScrollPositionOnUp) {
+ mScrollPositionOnUp = scrollX;
+ postDelayed(mSnapOnUpRunnable, SNAP_ON_UP_DELAY);
+ } else {
+ snapItems();
+ mSystemState = EventStates.SCROLLING;
+ }
+ }
+ };
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ int action = ev.getAction();
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ mSystemState = EventStates.FLING;
+ removeCallbacks(mSnapRunnable);
+ mScrollPositionOnUp = getScrollX();
+ postDelayed(mSnapOnUpRunnable, SNAP_ON_UP_DELAY);
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ mSystemState = EventStates.SCROLLING;
+ removeCallbacks(mSnapRunnable);
+ removeCallbacks(mSnapOnUpRunnable);
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ private void snapItems() {
+ Rect parentBounds = new Rect();
+ getDrawingRect(parentBounds);
+ Rect childBounds = new Rect();
+ ViewGroup parent = (ViewGroup) getChildAt(0);
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ View view = parent.getChildAt(i);
+ view.getHitRect(childBounds);
+ if (childBounds.right >= parentBounds.left && childBounds.left <= parentBounds.left) {
+ // First partially visible child
+ if ((childBounds.right - parentBounds.left) >=
+ (parentBounds.left - childBounds.left)) {
+ smoothScrollTo(Math.abs(childBounds.left), 0);
+ } else {
+ /**
+ * Added code to take into account dividers so that we do not see
+ * one on the edge of the screen when items snap in place.
+ */
+ int dividerWidth = 0;
+ if (parent instanceof LinearLayout) {
+ dividerWidth = ((LinearLayout) parent).getDividerWidth();
+ }
+ smoothScrollTo(Math.abs(childBounds.right) + dividerWidth, 0);
+ }
+ break;
+ }
+ }
+ }
+
+ // Overwrite measureChildX as we want our child to be able to tell the
+ // parents width but do not impose any limits (AT_MOST; AT_MAX)
+ @Override
+ protected void measureChild(View child, int parentWidthMeasureSpec,
+ int parentHeightMeasureSpec) {
+ ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+ int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop
+ + mPaddingBottom, lp.height);
+
+ int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ MeasureSpec.getSize(parentWidthMeasureSpec) - (mPaddingLeft + mPaddingRight),
+ MeasureSpec.UNSPECIFIED);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ @Override
+ protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
+ int parentHeightMeasureSpec, int heightUsed) {
+ MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+ mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ + heightUsed, lp.height);
+
+ int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ MeasureSpec.getSize(parentWidthMeasureSpec) -
+ (mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ + widthUsed), MeasureSpec.UNSPECIFIED);
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ if (mSystemState == EventStates.SCROLLING) {
+ return;
+ }
+ if (Math.abs(l - oldl) <= 1 && mSystemState == EventStates.FLING) {
+ removeCallbacks(mSnapRunnable);
+ postDelayed(mSnapRunnable, 100);
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/widget/BootAniImageView.java b/src/org/cyanogenmod/theme/widget/BootAniImageView.java
new file mode 100644
index 0000000..b3f58a6
--- /dev/null
+++ b/src/org/cyanogenmod/theme/widget/BootAniImageView.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.ImageView;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.zip.ZipFile;
+
+import libcore.io.IoUtils;
+
+import org.cyanogenmod.theme.util.BootAnimationHelper;
+
+public class BootAniImageView extends ImageView {
+ private static final String TAG = BootAniImageView.class.getName();
+
+ private static final boolean DEBUG = false;
+
+ private static final int MAX_BUFFERS = 2;
+
+ private Bitmap[] mBuffers = new Bitmap[MAX_BUFFERS];
+ private int mReadBufferIndex = 0;
+ private int mWriteBufferIndex = 0;
+ private ZipFile mBootAniZip;
+
+ private List<BootAnimationHelper.AnimationPart> mAnimationParts;
+ private int mCurrentPart;
+ private int mCurrentFrame;
+ private int mCurrentPartPlayCount;
+ private int mFrameDuration;
+
+ private boolean mActive = false;
+
+ public BootAniImageView(Context context) {
+ this(context, null);
+ }
+
+ public BootAniImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BootAniImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ if (visibility == VISIBLE) {
+ if (mBootAniZip != null) start();
+ } else {
+ stop();
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // In case we end up in the mid dle of onDraw while the buffers are being recycled
+ // we catch the exception and just let frame not be rendered.
+ try {
+ super.onDraw(canvas);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Unable to draw boot animation frame.");
+ }
+ }
+
+ public synchronized boolean setBootAnimation(ZipFile bootAni) {
+ if (bootAni == null) {
+ Log.w(TAG, "Boot animation ZipFile is null.");
+ return false;
+ }
+ // make sure we are stopped first
+ stop();
+
+ if (mBootAniZip != null) {
+ IoUtils.closeQuietly(mBootAniZip);
+ // This boot animation may be a different size than the previous
+ // one so clear out the buffers so they can be recreated with
+ // the correct size.
+ for (int i = 0; i < MAX_BUFFERS; i++) {
+ if (mBuffers[i] != null) {
+ mBuffers[i].recycle();
+ mBuffers[i] = null;
+ }
+ }
+ }
+ mBootAniZip = bootAni;
+
+ try {
+ mAnimationParts = BootAnimationHelper.parseAnimation(mBootAniZip);
+ } catch (Exception e) {
+ // "Gotta catch 'em all"
+ Log.e(TAG, "Unable to set boot animation", e);
+ mAnimationParts = null;
+ mBootAniZip = null;
+ return false;
+ }
+
+ if (mAnimationParts == null || mAnimationParts.size() == 0) {
+ return false;
+ }
+
+ final BootAnimationHelper.AnimationPart part = mAnimationParts.get(0);
+ mCurrentPart = 0;
+ mCurrentPartPlayCount = part.playCount;
+ mFrameDuration = part.frameRateMillis;
+ mWriteBufferIndex = mReadBufferIndex = 0;
+ mCurrentFrame = 0;
+
+ getNextFrame();
+
+ return true;
+ }
+
+ private void getNextFrame() {
+ if (mAnimationParts == null || mAnimationParts.size() == 0) return;
+
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inBitmap = mBuffers[mWriteBufferIndex];
+ opts.inPreferredConfig = Bitmap.Config.RGB_565;
+ opts.inMutable = true;
+ final BootAnimationHelper.AnimationPart part = mAnimationParts.get(mCurrentPart);
+ try {
+ mBuffers[mWriteBufferIndex] =
+ BitmapFactory.decodeStream(mBootAniZip.getInputStream(mBootAniZip.getEntry(
+ part.frames.get(mCurrentFrame++))), null, opts);
+ } catch (IllegalArgumentException iae) {
+ // In case we're here because the bitmap could not be re-used, try creating a new one
+ opts.inBitmap = null;
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "Trying to load frame without reusing existing bitmap", iae);
+ }
+ if (mBuffers[mWriteBufferIndex] != null) {
+ // clean up our old bitmap
+ mBuffers[mWriteBufferIndex].recycle();
+ mBuffers[mWriteBufferIndex] = null;
+ }
+ mBuffers[mWriteBufferIndex] =
+ BitmapFactory.decodeStream(mBootAniZip.getInputStream(mBootAniZip.getEntry(
+ part.frames.get(mCurrentFrame++))), null, opts);
+ } catch (Exception e) {
+ // Still failling? Let's log it and carry on.
+ Log.w(TAG, "Unable to get next frame", e);
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Unable to get next frame", e);
+ }
+ mWriteBufferIndex = (mWriteBufferIndex + 1) % MAX_BUFFERS;
+ if (mCurrentFrame >= part.frames.size()) {
+ if (mCurrentPartPlayCount > 0) {
+ if (--mCurrentPartPlayCount == 0) {
+ mCurrentPart++;
+ if (mCurrentPart >= mAnimationParts.size()) mCurrentPart = 0;
+ mCurrentFrame = 0;
+ mCurrentPartPlayCount = mAnimationParts.get(mCurrentPart).playCount;
+ } else {
+ mCurrentFrame = 0;
+ }
+ } else {
+ mCurrentFrame = 0;
+ }
+ }
+ }
+
+ public void start() {
+ if (mAnimationParts == null) return;
+
+ mActive = true;
+ post(mUpdateAnimationRunnable);
+ }
+
+ public void stop() {
+ mActive = false;
+ removeCallbacks(mUpdateImageRunnable);
+ removeCallbacks(mUpdateAnimationRunnable);
+ }
+
+ private Runnable mUpdateAnimationRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (!mActive) return;
+ BootAniImageView.this.postDelayed(mUpdateAnimationRunnable, mFrameDuration);
+ if (!isOffScreen()) {
+ BootAniImageView.this.post(mUpdateImageRunnable);
+ mReadBufferIndex = (mReadBufferIndex + 1) % MAX_BUFFERS;
+ getNextFrame();
+ }
+ }
+ };
+
+ private Runnable mUpdateImageRunnable = new Runnable() {
+ @Override
+ public void run() {
+ setImageBitmap(mBuffers[mReadBufferIndex]);
+ }
+ };
+
+ private boolean isOffScreen() {
+ int[] pos = new int[2];
+ getLocationOnScreen(pos);
+ return pos[1] >= mContext.getResources().getDisplayMetrics().heightPixels;
+ }
+}
diff --git a/src/org/cyanogenmod/theme/widget/ConfirmCancelOverlay.java b/src/org/cyanogenmod/theme/widget/ConfirmCancelOverlay.java
new file mode 100644
index 0000000..e00e67a
--- /dev/null
+++ b/src/org/cyanogenmod/theme/widget/ConfirmCancelOverlay.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import org.cyanogenmod.theme.chooser.R;
+
+public class ConfirmCancelOverlay extends FrameLayout {
+
+ private View mAcceptButton;
+ private View mCancelButton;
+ private TextView mTitle;
+
+ private OnOverlayDismissedListener mListener;
+
+ public ConfirmCancelOverlay(Context context) {
+ this(context, null);
+ }
+
+ public ConfirmCancelOverlay(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ConfirmCancelOverlay(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mAcceptButton = findViewById(R.id.accept);
+ mCancelButton = findViewById(R.id.cancel);
+ mTitle = (TextView) findViewById(R.id.overlay_title);
+
+ mAcceptButton.setOnClickListener(mClickListener);
+ mCancelButton.setOnClickListener(mClickListener);
+ }
+
+ public void setTitle(CharSequence title) {
+ mTitle.setText(title);
+ }
+
+ public void setTitle(int resId) {
+ mTitle.setText(resId);
+ }
+
+ public void setOnOverlayDismissedListener(OnOverlayDismissedListener listener) {
+ mListener = listener;
+ }
+
+ public void dismiss() {
+ if (mListener != null) {
+ mListener.onDismissed(false);
+ }
+ }
+
+ private OnClickListener mClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mListener != null) {
+ mListener.onDismissed(v == mAcceptButton);
+ }
+ }
+ };
+
+ public interface OnOverlayDismissedListener {
+ public void onDismissed(boolean accepted);
+ }
+}
diff --git a/src/org/cyanogenmod/theme/widget/FittedTextView.java b/src/org/cyanogenmod/theme/widget/FittedTextView.java
new file mode 100644
index 0000000..2451d25
--- /dev/null
+++ b/src/org/cyanogenmod/theme/widget/FittedTextView.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.content.Context;
+import android.graphics.Paint;
+import android.text.method.TransformationMethod;
+import android.text.method.AllCapsTransformationMethod;
+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;
+ //If set to true, the text will be resized to fit the view.
+ private boolean mAutoFitText = true;
+ //Used to instruct whether the text should be expanded to fill out the view, even if the text
+ //fits without being resized
+ private boolean mAutoExpand = true;
+
+ public FittedTextView(Context context) {
+ this(context, null);
+ }
+
+ public FittedTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FittedTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mPaint = new Paint();
+ }
+
+ protected void setAutoFitText(boolean autoFit) {
+ mAutoFitText = autoFit;
+ }
+
+ protected boolean getAutoFitText() {
+ return mAutoFitText;
+ }
+
+ protected void setAutoExpand(boolean autoExpand) {
+ mAutoExpand = autoExpand;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (!mAutoFitText) return;
+
+ final float THRESHOLD = 0.5f;
+ final float TARGET_WIDTH = getMeasuredWidth();
+ String text = getText().toString();
+ TransformationMethod tm = getTransformationMethod();
+ if (tm != null && tm instanceof AllCapsTransformationMethod) {
+ text = getText().toString().toUpperCase();
+ }
+ mPaint.set(getPaint());
+
+ if (mPaint.measureText(text) <= TARGET_WIDTH && !mAutoExpand) return;
+
+ 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/widget/LatoTextView.java b/src/org/cyanogenmod/theme/widget/LatoTextView.java
new file mode 100644
index 0000000..0cb02ae
--- /dev/null
+++ b/src/org/cyanogenmod/theme/widget/LatoTextView.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+
+import org.cyanogenmod.theme.chooser.R;
+import org.cyanogenmod.theme.util.Utils;
+
+import java.io.File;
+
+/**
+ * A custom TextView that always uses the Lato font
+ */
+public class LatoTextView extends FittedTextView {
+ private static final int NUM_TYPEFACE_PER_FAMILY = 4;
+
+ private static final String FONT_ASSSET_DIR = "fonts";
+ // Regular fonts
+ private static final String LATO_REGULAR_PATH =
+ FONT_ASSSET_DIR + File.separator + "Lato-Regular.ttf";
+ private static final String LATO_REGULAR_BOLD_PATH =
+ FONT_ASSSET_DIR + File.separator + "Lato-RegBold.ttf";
+ private static final String LATO_REGULAR_ITALIC_PATH =
+ FONT_ASSSET_DIR + File.separator + "Lato-RegItalic.otf";
+ private static final String LATO_REGULAR_BOLD_ITALIC_PATH =
+ FONT_ASSSET_DIR + File.separator + "Lato-RegBoldItalic.ttf";
+ // Condensed fonts
+ private static final String LATO_CONDENSED_PATH =
+ FONT_ASSSET_DIR + File.separator + "Lato-Cond.ttf";
+ private static final String LATO_CONDENSED_BOLD_PATH =
+ FONT_ASSSET_DIR + File.separator + "Lato-CondBold.ttf";
+ private static final String LATO_CONDENSED_ITALIC_PATH =
+ FONT_ASSSET_DIR + File.separator + "Lato-CondItalic.ttf";
+ private static final String LATO_CONDENSED_BOLD_ITALIC_PATH =
+ FONT_ASSSET_DIR + File.separator + "Lato-CondBoldItalic.ttf";
+ // Light fonts
+ private static final String LATO_LIGHT_PATH =
+ FONT_ASSSET_DIR + File.separator + "Lato-Light.ttf";
+ private static final String LATO_LIGHT_ITALIC_PATH =
+ FONT_ASSSET_DIR + File.separator + "Lato-LightItalic.ttf";
+
+ private static final String CONDENSED = "condensed";
+ private static final String LIGHT = "light";
+
+ private static Typeface[] sLatoRegularTypeface;
+ private static Typeface[] sLatoCondensedTypeface;
+ private static Typeface[] sLatoLightTypeface;
+ private static final Object sLock = new Object();
+
+ // Retrieving these attributes is done via reflection so let's just do this once and share
+ // it amongst the other instances of LatoTextView
+ private static int[] sTextViewStyleAttributes;
+
+ public LatoTextView(Context context) {
+ this(context, null);
+ }
+
+ public LatoTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public LatoTextView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ synchronized (sLock) {
+ AssetManager assets = context.getAssets();
+ if (sLatoRegularTypeface == null) {
+ sLatoRegularTypeface = new Typeface[NUM_TYPEFACE_PER_FAMILY];
+ sLatoRegularTypeface[Typeface.NORMAL] =
+ Typeface.createFromAsset(assets, LATO_REGULAR_PATH);
+ sLatoRegularTypeface[Typeface.BOLD] =
+ Typeface.createFromAsset(assets, LATO_REGULAR_BOLD_PATH);
+ sLatoRegularTypeface[Typeface.ITALIC] =
+ Typeface.createFromAsset(assets, LATO_REGULAR_ITALIC_PATH);
+ sLatoRegularTypeface[Typeface.BOLD_ITALIC] =
+ Typeface.createFromAsset(assets, LATO_REGULAR_BOLD_ITALIC_PATH);
+ }
+ if (sLatoCondensedTypeface == null) {
+ sLatoCondensedTypeface = new Typeface[NUM_TYPEFACE_PER_FAMILY];
+ sLatoCondensedTypeface[Typeface.NORMAL] =
+ Typeface.createFromAsset(assets, LATO_CONDENSED_PATH);
+ sLatoCondensedTypeface[Typeface.BOLD] =
+ Typeface.createFromAsset(assets, LATO_CONDENSED_BOLD_PATH);
+ sLatoCondensedTypeface[Typeface.ITALIC] =
+ Typeface.createFromAsset(assets, LATO_CONDENSED_ITALIC_PATH);
+ sLatoCondensedTypeface[Typeface.BOLD_ITALIC] =
+ Typeface.createFromAsset(assets, LATO_CONDENSED_BOLD_ITALIC_PATH);
+ }
+ if (sLatoLightTypeface == null) {
+ sLatoLightTypeface = new Typeface[NUM_TYPEFACE_PER_FAMILY];
+ sLatoLightTypeface[Typeface.NORMAL] =
+ Typeface.createFromAsset(assets, LATO_LIGHT_PATH);
+ sLatoLightTypeface[Typeface.BOLD] =
+ sLatoRegularTypeface[Typeface.BOLD];
+ sLatoLightTypeface[Typeface.ITALIC] =
+ Typeface.createFromAsset(assets, LATO_LIGHT_ITALIC_PATH);
+ sLatoLightTypeface[Typeface.BOLD_ITALIC] =
+ sLatoRegularTypeface[Typeface.BOLD_ITALIC];
+ }
+ }
+
+ final Resources.Theme theme = context.getTheme();
+ if (sTextViewStyleAttributes == null) {
+ sTextViewStyleAttributes =
+ Utils.getResourceDeclareStyleableIntArray("com.android.internal", "TextView");
+ }
+
+ if (sTextViewStyleAttributes != null) {
+ TypedArray a =
+ theme.obtainStyledAttributes(attrs, sTextViewStyleAttributes, defStyle, 0);
+ String fontFamily = "sans-serif";
+ int styleIndex = Typeface.NORMAL;
+ if (a != null) {
+ int n = a.getIndexCount();
+ for (int i = 0; i < n; i++) {
+ int attr = a.getIndex(i);
+
+ final Resources res = getResources();
+ int attrFontFamily =
+ res.getIdentifier("TextView_fontFamily", "styleable", "android");
+ int attrTextStyle =
+ res.getIdentifier("TextView_textStyle", "styleable", "android");
+ if (attr == attrFontFamily) {
+ fontFamily = a.getString(attr);
+ } else if (attr == attrTextStyle) {
+ styleIndex = a.getInt(attr, styleIndex);
+ }
+ }
+ a.recycle();
+ }
+
+ setTypefaceFromAttrs(fontFamily, styleIndex);
+ setAutoExpand(false);
+ TypedArray styledAttrs = context.obtainStyledAttributes(attrs,
+ R.styleable.FittedTextView, 0, 0);
+ try {
+ //Although we extend FittedTextView, we don't want all instances to auto fit the
+ //text, so we check if autoFitText has been set in the attributes. Default to false
+ boolean fit = styledAttrs.getBoolean(R.styleable.FittedTextView_autoFitText, false);
+ setAutoFitText(fit);
+ } finally {
+ styledAttrs.recycle();
+ }
+ }
+ }
+
+ private void setTypefaceFromAttrs(String familyName, int styleIndex) {
+ Typeface tf = null;
+ if (familyName != null) {
+ Typeface[] typefaces = sLatoRegularTypeface;
+ if (familyName.contains(CONDENSED)) {
+ typefaces = sLatoCondensedTypeface;
+ } else if (familyName.contains(LIGHT)) {
+ typefaces = sLatoLightTypeface;
+ }
+ tf = typefaces[styleIndex];
+ if (tf != null) {
+ setTypeface(tf);
+ return;
+ }
+ }
+ setTypeface(tf, styleIndex);
+ }
+}
diff --git a/src/org/cyanogenmod/theme/widget/LockableScrollView.java b/src/org/cyanogenmod/theme/widget/LockableScrollView.java
new file mode 100644
index 0000000..b4c9ffc
--- /dev/null
+++ b/src/org/cyanogenmod/theme/widget/LockableScrollView.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.ScrollView;
+
+public class LockableScrollView extends ScrollView {
+ private boolean mScrollingEnabled = true;
+
+ public LockableScrollView(Context context) {
+ this(context, null);
+ }
+
+ public LockableScrollView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public LockableScrollView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setScrollingEnabled(boolean enabled) {
+ mScrollingEnabled = enabled;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ return mScrollingEnabled && super.onInterceptTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ return mScrollingEnabled && super.onTouchEvent(ev);
+ default:
+ return super.onTouchEvent(ev);
+ }
+ }
+
+ @Override
+ public void setOverScrollMode(int mode) {
+ // Some themes can cause theme chooser to crash when creating the EdgeEffects for
+ // the scroll view. If an exception occurs we fallback to no overscroll
+ try {
+ super.setOverScrollMode(mode);
+ } catch (Exception e) {
+ super.setOverScrollMode(OVER_SCROLL_NEVER);
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/widget/NavBarSpace.java b/src/org/cyanogenmod/theme/widget/NavBarSpace.java
new file mode 100644
index 0000000..720df29
--- /dev/null
+++ b/src/org/cyanogenmod/theme/widget/NavBarSpace.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import org.cyanogenmod.theme.util.Utils;
+
+/**
+ * A simple view used to pad layouts so that content floats above the
+ * navigation bar. This is best used with transparent or translucent
+ * navigation bars where the content can go behind them.
+ */
+public class NavBarSpace extends View {
+
+ public NavBarSpace(Context context) {
+ this(context, null);
+ }
+
+ public NavBarSpace(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public NavBarSpace(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ if (!Utils.hasNavigationBar(getContext())) {
+ this.setVisibility(View.GONE);
+ } else {
+ this.setVisibility(View.VISIBLE);
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/theme/widget/ThemeTagLayout.java b/src/org/cyanogenmod/theme/widget/ThemeTagLayout.java
new file mode 100644
index 0000000..18e3da2
--- /dev/null
+++ b/src/org/cyanogenmod/theme/widget/ThemeTagLayout.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 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.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.cyanogenmod.theme.chooser.R;
+
+public class ThemeTagLayout extends LinearLayout {
+ private ImageView mAppliedTag;
+ private TextView mCustomizedTag;
+ private TextView mUpdatedTag;
+ private TextView mDefaultTag;
+ private TextView mLegacyTag;
+
+ public ThemeTagLayout(Context context) {
+ this(context, null);
+ }
+
+ public ThemeTagLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ThemeTagLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ LayoutInflater inflater =
+ (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mAppliedTag = (ImageView) inflater.inflate(R.layout.tag_applied, this, false);
+ mCustomizedTag = (TextView) inflater.inflate(R.layout.tag_customized, this, false);
+ mUpdatedTag = (TextView) inflater.inflate(R.layout.tag_updated, this, false);
+ mDefaultTag = (TextView) inflater.inflate(R.layout.tag_default, this, false);
+ mLegacyTag = (TextView) inflater.inflate(R.layout.tag_legacy, this, false);
+ }
+
+ public void setAppliedTagEnabled(boolean enabled) {
+ if (enabled) {
+ if (findViewById(R.id.tag_applied) != null) return;
+ addView(mAppliedTag, 0);
+ } else {
+ if (findViewById(R.id.tag_applied) == null) return;
+ removeView(mAppliedTag);
+ }
+ }
+
+ public void setCustomizedTagEnabled(boolean enabled) {
+ if (enabled) {
+ if (findViewById(R.id.tag_customized) != null) return;
+ final int childCount = getChildCount();
+ if (childCount > 1) {
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child != mAppliedTag) {
+ addView(mCustomizedTag, i);
+ break;
+ }
+ }
+ } else {
+ addView(mCustomizedTag);
+ }
+ } else {
+ if (findViewById(R.id.tag_customized) == null) return;
+ removeView(mCustomizedTag);
+ }
+ }
+
+ public boolean isCustomizedTagEnabled() {
+ return findViewById(R.id.tag_customized) != null;
+ }
+
+ public void setUpdatedTagEnabled(boolean enabled) {
+ if (enabled) {
+ if (findViewById(R.id.tag_updated) != null) return;
+ final int childCount = getChildCount();
+ if (childCount > 2) {
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child != mAppliedTag && child != mCustomizedTag) {
+ addView(mUpdatedTag, i);
+ break;
+ }
+ }
+ } else {
+ addView(mUpdatedTag);
+ }
+ } else {
+ if (findViewById(R.id.tag_updated) == null) return;
+ removeView(mUpdatedTag);
+ }
+ }
+
+ public boolean isUpdatedTagEnabled() {
+ return findViewById(R.id.tag_updated) != null;
+ }
+
+ public void setDefaultTagEnabled(boolean enabled) {
+ if (enabled) {
+ if (findViewById(R.id.tag_default) != null) return;
+ addView(mDefaultTag);
+ } else {
+ if (findViewById(R.id.tag_default) == null) return;
+ removeView(mDefaultTag);
+ }
+ }
+
+ public void setLegacyTagEnabled(boolean enabled) {
+ if (enabled) {
+ if (findViewById(R.id.tag_legacy) != null) return;
+ addView(mLegacyTag);
+ } else {
+ if (findViewById(R.id.tag_legacy) == null) return;
+ removeView(mLegacyTag);
+ }
+ }
+}