diff options
Diffstat (limited to 'src/com/cyngn/theme')
20 files changed, 5637 insertions, 0 deletions
diff --git a/src/com/cyngn/theme/chooser/AppReceiver.java b/src/com/cyngn/theme/chooser/AppReceiver.java new file mode 100644 index 0000000..ef39669 --- /dev/null +++ b/src/com/cyngn/theme/chooser/AppReceiver.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.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 com.cyngn.theme.util.NotificationHelper; + +public class AppReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + Uri uri = intent.getData(); + String pkgName = uri != null ? uri.getSchemeSpecificPart() : null; + String action = intent.getAction(); + boolean isReplacing = intent.getExtras().getBoolean(Intent.EXTRA_REPLACING, false); + + if (Intent.ACTION_PACKAGE_ADDED.equals(action) && !isReplacing) { + try { + if (isTheme(context, pkgName)) { + NotificationHelper.postThemeInstalledNotification(context, pkgName); + } + } catch (NameNotFoundException e) { + } + } else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) { + NotificationHelper.cancelNotificationForPackage(context, pkgName); + } + } + + private boolean isTheme(Context context, String pkgName) throws NameNotFoundException { + PackageInfo pi = context.getPackageManager().getPackageInfo(pkgName, 0); + if (pi == null) return false; + + if ((pi.themeInfos != null && pi.themeInfos.length > 0) || + (pi.legacyThemeInfos != null && pi.legacyThemeInfos.length > 0)) { + return true; + } + + return false; + } +} diff --git a/src/com/cyngn/theme/chooser/ChooserActivity.java b/src/com/cyngn/theme/chooser/ChooserActivity.java new file mode 100644 index 0000000..13cdbcb --- /dev/null +++ b/src/com/cyngn/theme/chooser/ChooserActivity.java @@ -0,0 +1,489 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.theme.chooser; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.ThemeConfig; +import android.content.res.ThemeManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.provider.ThemesContract; +import android.provider.ThemesContract.ThemesColumns; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.view.ThemeViewPager; +import android.support.v4.view.ViewPager; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.Button; + +import com.cyngn.theme.util.TypefaceHelperCache; +import com.cyngn.theme.util.Utils; + +import java.util.HashMap; +import java.util.Map; + +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_ALARMS; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_BOOT_ANIM; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_NOTIFICATIONS; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_RINGTONES; + +public class ChooserActivity extends FragmentActivity + implements LoaderManager.LoaderCallbacks<Cursor>, ThemeManager.ThemeChangeListener { + public static final String DEFAULT = ThemeConfig.HOLO_DEFAULT; + public static final int REQUEST_UNINSTALL = 1; // Request code + + private static final long SLIDE_CONTENT_ANIM_DURATION = 300L; + private static final long MOVE_TO_MY_THEME_DELAY = 750L; + + private static final int OFFSCREEN_PAGE_LIMIT = 3; + + private static final int LOADER_ID_INSTALLED_THEMES = 1000; + private static final int LOADER_ID_APPLIED = 1001; + + private PagerContainer mContainer; + private ThemeViewPager mPager; + + private ThemesAdapter mAdapter; + private ThemeManager mService; + private boolean mExpanded = false; + private ComponentSelector mSelector; + private View mSaveApplyLayout; + private int mContainerYOffset = 0; + private TypefaceHelperCache mTypefaceHelperCache; + private boolean mIsAnimating; + private Handler mHandler; + + // Current system theme configuration as component -> pkgName + private Map<String, String> mCurrentTheme = new HashMap<String, String>(); + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mContainer = (PagerContainer) findViewById(R.id.pager_container); + mPager = (ThemeViewPager) findViewById(R.id.viewpager); + + mPager.setOnClickListener(mPagerClickListener); + mAdapter = new ThemesAdapter(this); + 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); + + mService = (ThemeManager) getSystemService(Context.THEME_SERVICE); + getSupportLoaderManager().restartLoader(LOADER_ID_APPLIED, null, this); + + mSaveApplyLayout = findViewById(R.id.save_apply_layout); + if (!Utils.hasNavigationBar(this)) { + mSaveApplyLayout.findViewById(R.id.navbar_padding).setVisibility(View.GONE); + } + mSaveApplyLayout.findViewById(R.id.save_apply_button).setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mIsAnimating) return; + hideSaveApplyButton(); + mExpanded = false; + mContainer.setClickable(false); + final ThemeFragment f = getCurrentFragment(); + f.fadeOutCards(new Runnable() { + public void run() { + mContainer.collapse(); + f.collapse(true); + } + }); + setAnimatingStateAndScheduleFinish(); + } + }); + mTypefaceHelperCache = TypefaceHelperCache.getInstance(); + mHandler = new Handler(); + } + + public void hideSaveApplyButton() { + 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 setAnimatingStateAndScheduleFinish() { + mIsAnimating = true; + mContainer.setIsAnimating(true); + mHandler.postDelayed(new Runnable() { + public void run() { + mIsAnimating = false; + mContainer.setIsAnimating(false); + } + }, ThemeFragment.ANIMATE_START_DELAY + ThemeFragment.ANIMATE_DURATION); + } + + /** + * Disable the ViewPager while a theme change is occuring + */ + public void themeChangeStarted() { + mPager.setEnabled(false); + } + + /** + * Re-enable the ViewPager and update the "My theme" fragment if available + */ + public void themeChangeEnded() { + if (mPager.getCurrentItem() != 0) { + ThemeFragment f; + if (mPager.getCurrentItem() <= OFFSCREEN_PAGE_LIMIT) { + // Clear the "My theme" card so it loads the newly applied changes + f = (ThemeFragment) mAdapter.instantiateItem(mPager, 0); + if (f != null) f.clearChanges(); + } + + // clear the current card so it returns to it's previous state + f = getCurrentFragment(); + if (f != null) f.clearChanges(); + mPager.postDelayed(new Runnable() { + @Override + public void run() { + mPager.setCurrentItem(0, true); + } + }, MOVE_TO_MY_THEME_DELAY); + } + mPager.setEnabled(true); + } + + public ComponentSelector getComponentSelector() { + return mSelector; + } + + public void showComponentSelector(String component, 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) hideSaveApplyButton(); + mSelector.show(component, itemsPerPage, height); + + // determine if we need to shift the cards up + int[] coordinates = new int[2]; + v.getLocationOnScreen(coordinates); + coordinates[1] += v.getHeight(); + int top = getWindowManager().getDefaultDisplay().getHeight() - height; + if (coordinates[1] > top) { + slideContentUp(top - coordinates[1]); + } + } + } + + private void slideContentUp(int yDelta) { + yDelta -= getResources().getDimensionPixelSize(R.dimen.content_offset_padding); + mContainerYOffset = yDelta; + mContainer.animate().translationYBy(yDelta).setDuration(SLIDE_CONTENT_ANIM_DURATION); + } + + private void slideContentDown(final int yDelta) { + mContainer.animate().translationYBy(-yDelta).setDuration(SLIDE_CONTENT_ANIM_DURATION); + } + + @Override + protected void onResume() { + super.onResume(); + mService.onClientResumed(this); + getSupportLoaderManager().restartLoader(LOADER_ID_APPLIED, null, this); + } + + @Override + public void onBackPressed() { + if (mSelector.isEnabled()) { + mSelector.hide(); + if (mContainerYOffset != 0) { + slideContentDown(mContainerYOffset); + mContainerYOffset = 0; + } + final ThemeFragment f = getCurrentFragment(); + f.fadeInCards(); + } else if (mExpanded) { + if (mIsAnimating) { + return; + } + + if (mSaveApplyLayout.getVisibility() == View.VISIBLE) { + hideSaveApplyButton(); + getCurrentFragment().clearChanges(); + } + mExpanded = false; + final ThemeFragment f = getCurrentFragment(); + f.fadeOutCards(new Runnable() { + public void run() { + mContainer.collapse(); + f.collapse(false); + } + }); + setAnimatingStateAndScheduleFinish(); + } else { + super.onBackPressed(); + } + } + + @Override + public void onPause() { + super.onPause(); + mService.onClientPaused(this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mService.onClientDestroyed(this); + } + + @Override + protected void onStart() { + super.onStart(); + if (mTypefaceHelperCache.getTypefaceCount() <= 0) { + new TypefacePreloadTask().execute(); + } + } + + private View.OnClickListener mPagerClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (!mExpanded && !mIsAnimating) { + mExpanded = true; + mContainer.setClickable(false); + mContainer.expand(); + ThemeFragment f = getCurrentFragment(); + f.expand(); + setAnimatingStateAndScheduleFinish(); + } + } + }; + + private ComponentSelector.OnOpenCloseListener mOpenCloseListener = new ComponentSelector.OnOpenCloseListener() { + @Override + public void onSelectorOpened() { + } + + @Override + public void onSelectorClosed() { + ThemeFragment f = getCurrentFragment(); + if (f.componentsChanged()) { + mSaveApplyLayout.setVisibility(View.VISIBLE); + mSaveApplyLayout.startAnimation(AnimationUtils.loadAnimation(ChooserActivity.this, + R.anim.component_selection_animate_in)); + } + } + }; + + 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. + return (ThemeFragment) mAdapter.instantiateItem(mPager, mPager.getCurrentItem()); + } + + private void populateCurrentTheme(Cursor c) { + c.moveToPosition(-1); + while(c.moveToNext()) { + int mixkeyIdx = c.getColumnIndex(ThemesContract.MixnMatchColumns.COL_KEY); + int pkgIdx = c.getColumnIndex(ThemesContract.MixnMatchColumns.COL_VALUE); + String mixkey = c.getString(mixkeyIdx); + String component = ThemesContract.MixnMatchColumns.mixNMatchKeyToComponent(mixkey); + String pkg = c.getString(pkgIdx); + mCurrentTheme.put(component, pkg); + } + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + 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.) + mAdapter.swapCursor(data); + mAdapter.notifyDataSetChanged(); + break; + case LOADER_ID_APPLIED: + getSupportLoaderManager().restartLoader(LOADER_ID_INSTALLED_THEMES, null, this); + 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) { + String selection = null; + String selectionArgs[] = null; + String sortOrder = null; + Uri contentUri = null; + + switch (id) { + case LOADER_ID_INSTALLED_THEMES: + selection = ThemesColumns.PRESENT_AS_THEME + "=?"; + selectionArgs = new String[] { "1" }; + // sort in ascending order but make sure the "default" theme is always first + sortOrder = "(" + ThemesColumns.IS_DEFAULT_THEME + "=1) DESC, " + + ThemesColumns.TITLE + " ASC"; + contentUri = ThemesColumns.CONTENT_URI; + 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(this, contentUri, null, selection, + selectionArgs, sortOrder); + } + + @Override + public void onProgress(int progress) { + + } + + @Override + public void onFinish(boolean isSuccess) { + + } + + public class ThemesAdapter extends FragmentStatePagerAdapter { + private Cursor mCursor; + private Context mContext; + + public ThemesAdapter(Context context) { + super(getSupportFragmentManager()); + mContext = context; + } + + @Override + public Fragment getItem(int position) { + String pkgName; + if (position == 0) { + pkgName = ThemeFragment.CURRENTLY_APPLIED_THEME; + } else { + mCursor.moveToPosition(position - 1); + int pkgIdx = mCursor.getColumnIndex(ThemesColumns.PKG_NAME); + pkgName = mCursor.getString(pkgIdx); + } + ThemeFragment f = ThemeFragment.newInstance(pkgName); + f.setCurrentTheme(mCurrentTheme); + return f; + } + + @Override + public int getItemPosition(Object object) { + ThemeFragment fragment = (ThemeFragment) object; + if (fragment.isUninstalled()) { + return POSITION_NONE; + } + 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 mCursor == null ? 1 : mCursor.getCount() + 1; + } + + public void swapCursor(Cursor c) { + mCursor = c; + } + } + + 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; + } + } +} diff --git a/src/com/cyngn/theme/chooser/ComponentCardView.java b/src/com/cyngn/theme/chooser/ComponentCardView.java new file mode 100644 index 0000000..555fc7a --- /dev/null +++ b/src/com/cyngn/theme/chooser/ComponentCardView.java @@ -0,0 +1,202 @@ +package com.cyngn.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.2f; + private static final int BACKGROUND_SEMI_OPAQUE_ALPHA = (int) (256.0f * SEMI_OPAQUE_ALPHA); + + protected TextView mLabel; + + // Expanded Padding + int mExpandPadLeft; + int mExpandPadTop; + int mExpandPadRight; + int mExpandPadBottom; + + 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); + } + + @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(); + } + + public void expand(boolean showLabel) { + TransitionDrawable bg = null; + if (getBackground() instanceof TransitionDrawable) { + bg = (TransitionDrawable) getBackground(); + } + if (bg != null) { + Rect paddingRect = new Rect(); + bg.getPadding(paddingRect); + } + + setPadding(mExpandPadLeft, mExpandPadTop, 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() { + 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() { + if (mLabel != null) { + mLabel.animate().alpha(SEMI_OPAQUE_ALPHA).setDuration(CARD_FADE_DURATION); + } + final ValueAnimator bgAlphaAnimator = ValueAnimator.ofObject(new IntEvaluator(), 255, + BACKGROUND_SEMI_OPAQUE_ALPHA); + bgAlphaAnimator.setDuration(CARD_FADE_DURATION); + bgAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + getBackground().setAlpha((Integer) animation.getAnimatedValue()); + } + }); + bgAlphaAnimator.start(); + } + + /** + * Animates the card background and the title back to full opacity. + */ + public void animateCardFadeIn() { + if (getBackground().getAlpha() > 51) return; + if (mLabel != null) { + mLabel.animate().alpha(1f).setDuration(CARD_FADE_DURATION); + } + final ValueAnimator bgAlphaAnimator = ValueAnimator.ofObject(new IntEvaluator(), + BACKGROUND_SEMI_OPAQUE_ALPHA, 255); + bgAlphaAnimator.setDuration(CARD_FADE_DURATION); + bgAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + getBackground().setAlpha((Integer) animation.getAnimatedValue()); + } + }); + bgAlphaAnimator.start(); + } + + /** + * 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 = (int) v.getX(); + final int y = (int) v.getY(); + 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(); + } +} diff --git a/src/com/cyngn/theme/chooser/ComponentSelector.java b/src/com/cyngn/theme/chooser/ComponentSelector.java new file mode 100644 index 0000000..a4dab26 --- /dev/null +++ b/src/com/cyngn/theme/chooser/ComponentSelector.java @@ -0,0 +1,765 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.theme.chooser; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.Cursor; +import android.graphics.Typeface; +import android.graphics.drawable.BitmapDrawable; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.os.Bundle; +import android.provider.Settings; +import android.provider.ThemesContract; +import android.provider.ThemesContract.MixnMatchColumns; +import android.provider.ThemesContract.PreviewColumns; +import android.provider.ThemesContract.ThemesColumns; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +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.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.viewpagerindicator.PageIndicator; +import com.cyngn.theme.util.AudioUtils; +import com.cyngn.theme.util.ThemedTypefaceHelper; +import com.cyngn.theme.util.TypefaceHelperCache; +import com.cyngn.theme.util.Utils; + +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_ALARMS; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_BOOT_ANIM; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_LAUNCHER; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_LOCKSCREEN; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_NOTIFICATIONS; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_OVERLAYS; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_RINGTONES; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_STATUS_BAR; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_NAVIGATION_BAR; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_ICONS; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_FONTS; + +public class ComponentSelector extends LinearLayout + implements LoaderManager.LoaderCallbacks<Cursor> { + private static final String TAG = ComponentSelector.class.getSimpleName(); + + public static final boolean DEBUG_SELECTOR = false; + + private static final int LOADER_ID_STATUS_BAR = 100; + private static final int LOADER_ID_NAVIGATION_BAR = 101; + private static final int LOADER_ID_FONT = 102; + private static final int LOADER_ID_ICON = 103; + private static final int LOADER_ID_STYLE = 104; + private static final int LOADER_ID_WALLPAPER = 105; + private static final int LOADER_ID_BOOTANIMATIONS = 106; + private static final int LOADER_ID_RINGTONE = 107; + private static final int LOADER_ID_NOTIFICATION = 108; + private static final int LOADER_ID_ALARM = 109; + private static final int LOADER_ID_LOCKSCREEN = 110; + + private Context mContext; + private LayoutInflater mInflater; + private ViewPager mPager; + + private String mComponentType; + private CursorPagerAdapter mAdapter; + private int mBatteryStyle; + private int mItemsPerPage; + private String mAppliedComponentPkgName; + + // 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; + + 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); + mBatteryStyle = Settings.System.getInt(context.getContentResolver(), + Settings.System.STATUS_BAR_BATTERY, 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) { + } + + @Override + public void onAnimationEnd(Animation animation) { + setVisibility(View.GONE); + if (mOpenCloseListener != null) mOpenCloseListener.onSelectorClosed(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + mMediaPlayer = new MediaPlayer(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mPager = (ViewPager) findViewById(R.id.pager); + mAdapter = new CursorPagerAdapter<View>(null, mItemsPerPage); + mPager.setAdapter(mAdapter); + PageIndicator indicator = (PageIndicator) findViewById(R.id.page_indicator); + indicator.setViewPager(mPager); + + // set navbar_padding to GONE if no on screen navigation bar is available + if (!Utils.hasNavigationBar(mContext)) { + findViewById(R.id.navbar_padding).setVisibility(View.GONE); + } + setEnabled(false); + } + + public void setComponentType(String component) { + // Find out which theme is currently applied for this component + String selection = MixnMatchColumns.COL_KEY + "=?"; + String[] selectionArgs = {MixnMatchColumns.componentToMixNMatchKey(component)}; + Cursor c = mContext.getContentResolver().query(MixnMatchColumns.CONTENT_URI, + null, selection, selectionArgs, null); + if (c != null) { + if (c.moveToFirst()) { + mAppliedComponentPkgName = c.getString( + c.getColumnIndex(MixnMatchColumns.COL_VALUE)); + } + c.close(); + } else { + mAppliedComponentPkgName = null; + } + if (mComponentType == null || !mComponentType.equals(component)){ + mAdapter.swapCursor(null); + } + mComponentType = component; + ((FragmentActivity) mContext).getSupportLoaderManager().restartLoader( + getLoaderIdFromComponent(component), null, this); + } + + public String getComponentType() { + return mComponentType; + } + + public void setNumItemsPerPage(int itemsPerPage) { + if (mItemsPerPage != itemsPerPage) { + mItemsPerPage = itemsPerPage; + mAdapter.setNumItemsPerPage(mItemsPerPage); + } + } + + public void setHeight(int height) { + ViewGroup.LayoutParams params = mPager.getLayoutParams(); + if (params.height != height) { + params.height = height; + mPager.setLayoutParams(params); + requestLayout(); + } + } + + public void show(String componentType, int itemsPerPage, int height) { + setNumItemsPerPage(itemsPerPage); + setHeight(height); + setComponentType(componentType); + 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_ICON; + } + 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_BOOTANIMATIONS; + } + 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) { + 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: + selection = MODIFIES_FONTS + "=?"; + break; + case LOADER_ID_ICON: + 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 + "=?"; + projection = new String[] { + PreviewColumns.WALLPAPER_THUMBNAIL, + ThemesColumns.TITLE, + ThemesColumns.PKG_NAME + }; + break; + case LOADER_ID_BOOTANIMATIONS: + 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 + "=?"; + projection = new String[] { + PreviewColumns.LOCK_WALLPAPER_THUMBNAIL, + ThemesColumns.TITLE, + 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(mContext, PreviewColumns.CONTENT_URI, + projection, selection, selectionArgs, sortOrder); + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, final Cursor data) { + mAdapter.swapCursor(data); + } + + @Override + public void onLoaderReset(Loader<Cursor> loader) { + mAdapter.swapCursor(null); + } + + public void setOnItemClickedListener(OnItemClickedListener listener) { + mListener = listener; + } + + public void setOnOpenCloseListener(OnOpenCloseListener listener) { + mOpenCloseListener = listener; + } + + public class CursorPagerAdapter<T extends View> extends PagerAdapter { + LinearLayout.LayoutParams mItemParams = + new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f); + LinearLayout.LayoutParams mSoundItemParams = + new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 0, 1.0f); + private Cursor mCursor; + private int mItemsPerPage; + private TypefaceHelperCache mTypefaceCache; + + public CursorPagerAdapter(Cursor cursor, int itemsPerPage) { + super(); + mCursor = cursor; + mItemsPerPage = itemsPerPage; + mTypefaceCache = TypefaceHelperCache.getInstance(); + } + + public void setNumItemsPerPage(int itemsPerPage) { + mItemsPerPage = itemsPerPage; + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + ViewGroup v = (ViewGroup) mInflater.inflate(R.layout.component_selection_pager_item, + container, false); + if (v instanceof LinearLayout) { + ((LinearLayout) v).setWeightSum(mItemsPerPage); + } + if (MODIFIES_STATUS_BAR.equals(mComponentType)) { + newStatusBarView(mCursor, v, position); + } + if (MODIFIES_NAVIGATION_BAR.equals(mComponentType)) { + newNavBarView(mCursor, v, position); + } + if (MODIFIES_FONTS.equals(mComponentType)) { + newFontView(mCursor, v, position); + } + if (MODIFIES_ICONS.equals(mComponentType)) { + newIconView(mCursor, v, position); + } + if (MODIFIES_OVERLAYS.equals(mComponentType)) { + newStyleView(mCursor, v, position); + } + if (MODIFIES_LAUNCHER.equals(mComponentType)) { + newWallpapersView(mCursor, v, position); + } + if (MODIFIES_BOOT_ANIM.equals(mComponentType)) { + newBootanimationView(mCursor, v, position); + } + if (MODIFIES_RINGTONES.equals(mComponentType) || + MODIFIES_NOTIFICATIONS.equals(mComponentType) || + MODIFIES_ALARMS.equals(mComponentType)) { + v = (ViewGroup) mInflater.inflate(R.layout.component_selection_sounds_pager_item, + container, false); + if (v instanceof LinearLayout) { + ((LinearLayout) v).setWeightSum(mItemsPerPage); + } + newSoundView(mCursor, v, position, mComponentType); + } + if (MODIFIES_LOCKSCREEN.equals(mComponentType)) { + newLockScreenView(mCursor, v, position); + } + container.addView(v); + return v; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + if (object instanceof View) { + container.removeView((View) object); + } + } + + @Override + public int getCount() { + return mCursor == null ? 0 : (int) Math.ceil((float)mCursor.getCount() / mItemsPerPage); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + @Override + public int getItemPosition(Object object) { + return POSITION_NONE; + } + + public void swapCursor(Cursor c) { + mCursor = c; + notifyDataSetChanged(); + mPager.setCurrentItem(0, false); + } + + public Cursor getCursor() { + return mCursor; + } + + private OnClickListener mItemClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + String pkgName = (String) v.getTag(); + if (DEBUG_SELECTOR) Toast.makeText(mContext, pkgName, Toast.LENGTH_SHORT).show(); + if (mListener != null) { + mListener.onItemClicked(pkgName); + } + } + }; + + private void newStatusBarView(Cursor cursor, ViewGroup parent, int position) { + 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.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(cursor.getString(pkgNameIndex)); + v.setOnClickListener(mItemClickListener); + parent.addView(v, mItemParams); + addDividerIfNeeded(parent, i, index, cursor); + } + } + + private void newNavBarView(Cursor cursor, ViewGroup parent, int position) { + 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.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(cursor.getString(pkgNameIndex)); + v.setOnClickListener(mItemClickListener); + parent.addView(v, mItemParams); + addDividerIfNeeded(parent, i, index, cursor); + } + } + + private void newFontView(Cursor cursor, ViewGroup parent, int position) { + 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.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(cursor.getString(pkgNameIndex)); + v.setOnClickListener(mItemClickListener); + parent.addView(v, mItemParams); + addDividerIfNeeded(parent, i, index, cursor); + } + } + + private void newIconView(Cursor cursor, ViewGroup parent, int position) { + 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.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(cursor.getString(pkgNameIndex)); + v.setOnClickListener(mItemClickListener); + parent.addView(v, mItemParams); + addDividerIfNeeded(parent, i, index, cursor); + } + } + + private void newStyleView(Cursor cursor, ViewGroup parent, int position) { + 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.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(cursor.getString(pkgNameIndex)); + v.setOnClickListener(mItemClickListener); + parent.addView(v, mItemParams); + addDividerIfNeeded(parent, i, index, cursor); + } + } + + private void newWallpapersView(Cursor cursor, ViewGroup parent, int position) { + 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.wallpaper_component_selection_item, parent, + false); + int wallpaperIndex = cursor.getColumnIndex(PreviewColumns.WALLPAPER_THUMBNAIL); + int pkgNameIndex = cursor.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME); + + ((ImageView) v.findViewById(R.id.icon)).setImageBitmap( + Utils.loadBitmapBlob(cursor, wallpaperIndex)); + setTitle(((TextView) v.findViewById(R.id.title)), cursor); + v.setTag(cursor.getString(pkgNameIndex)); + v.setOnClickListener(mItemClickListener); + parent.addView(v, mItemParams); + addDividerIfNeeded(parent, i, index, cursor); + } + } + + private void newLockScreenView(Cursor cursor, ViewGroup parent, int position) { + 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.wallpaper_component_selection_item, parent, + false); + int wallpaperIndex = cursor.getColumnIndex(PreviewColumns.LOCK_WALLPAPER_THUMBNAIL); + int pkgNameIndex = cursor.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME); + + ((ImageView) v.findViewById(R.id.icon)).setImageBitmap( + Utils.loadBitmapBlob(cursor, wallpaperIndex)); + setTitle(((TextView) v.findViewById(R.id.title)), cursor); + v.setTag(cursor.getString(pkgNameIndex)); + v.setOnClickListener(mItemClickListener); + parent.addView(v, mItemParams); + addDividerIfNeeded(parent, i, index, cursor); + } + } + + private void newBootanimationView(Cursor cursor, ViewGroup parent, int position) { + 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.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(cursor.getString(pkgNameIndex)); + v.setOnClickListener(mItemClickListener); + parent.addView(v, mItemParams); + addDividerIfNeeded(parent, i, index, cursor); + } + } + + private void newSoundView(Cursor cursor, ViewGroup parent, int position, String component) { + 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(cursor.getString(pkgNameIndex)); + v.setOnClickListener(mItemClickListener); + parent.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 (mComponentType.equals(MODIFIES_RINGTONES)) { + type = RingtoneManager.TYPE_RINGTONE; + } else if (mComponentType.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); + } + } + }); + } + } + + private void addDivider(ViewGroup parent) { + final Resources res = getResources(); + View v = mInflater.inflate(R.layout.component_divider, parent, false); + LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) v.getLayoutParams(); + // Boot animations require a taller divider so adjust accordingly + if (ThemesColumns.MODIFIES_BOOT_ANIM.equals(mComponentType)) { + params.topMargin = res.getDimensionPixelSize( + R.dimen.component_divider_margin_top_bootani); + params.height = res.getDimensionPixelSize(R.dimen.component_divider_height_bootani); + } + v.setLayoutParams(params); + parent.addView(v); + } + + private void addDividerIfNeeded(ViewGroup parent, int position, int cursorIndex, + Cursor cursor) { + if (position < mItemsPerPage - 1 && cursorIndex < cursor.getCount() - 1) { + addDivider(parent); + } + } + + private void setTitle(TextView titleView, Cursor cursor) { + String pkgName = cursor.getString(cursor.getColumnIndex(ThemesColumns.PKG_NAME)); + titleView.setText(cursor.getString(cursor.getColumnIndex(ThemesColumns.TITLE))); + if (pkgName.equals(mAppliedComponentPkgName)) { + titleView.setTextColor(getResources().getColor( + R.color.component_selection_current_text_color)); + } + } + } + + public interface OnItemClickedListener { + public void onItemClicked(String pkgName); + } + + public interface OnOpenCloseListener { + public void onSelectorOpened(); + public void onSelectorClosed(); + } +}
\ No newline at end of file diff --git a/src/com/cyngn/theme/chooser/IconTransitionDrawable.java b/src/com/cyngn/theme/chooser/IconTransitionDrawable.java new file mode 100644 index 0000000..a62fb07 --- /dev/null +++ b/src/com/cyngn/theme/chooser/IconTransitionDrawable.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyngn.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><transition></code> element. + * Each Drawable in the transition is defined in a nested <code><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/com/cyngn/theme/chooser/NotificationHijackingService.java b/src/com/cyngn/theme/chooser/NotificationHijackingService.java new file mode 100644 index 0000000..910a2de --- /dev/null +++ b/src/com/cyngn/theme/chooser/NotificationHijackingService.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.theme.chooser; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.provider.Settings; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; +import android.text.TextUtils; + +public class NotificationHijackingService extends NotificationListenerService { + private static final String TAG = NotificationHijackingService.class.getName(); + private static final String GOOGLE_PLAY_PACKAGE_NAME = "com.android.vending"; + private static final String ACTION_INSTALLED = + "com.android.vending.SUCCESSFULLY_INSTALLED_CLICKED"; + private static final String EXTRA_PACKAGE_NAME = "package_name"; + + @Override + public void onNotificationPosted(StatusBarNotification sbn) { + if (GOOGLE_PLAY_PACKAGE_NAME.equals(sbn.getPackageName())) { + PendingIntent contentIntent = sbn.getNotification().contentIntent; + if (contentIntent == null) return; + Intent intent = contentIntent.getIntent(); + if (intent == null) return; + String action = intent.getAction(); + if (ACTION_INSTALLED.equals(action)) { + String pkgName = intent.getStringExtra(EXTRA_PACKAGE_NAME); + try { + PackageInfo pi = getPackageManager().getPackageInfo(pkgName, 0); + if (pi != null) { + if ((pi.themeInfos != null && pi.themeInfos.length > 0) || + (pi.legacyThemeInfos != null && pi.legacyThemeInfos.length > 0)) { + cancelNotification(GOOGLE_PLAY_PACKAGE_NAME, sbn.getTag(), sbn.getId()); + } + } + } catch (PackageManager.NameNotFoundException e) { + } + } + } + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn) { + } + + // ensure that this notification listener is enabled. + // the service watches for google play notifications + public static void ensureEnabled(Context context) { + ComponentName me = new ComponentName(context, NotificationHijackingService.class); + String meFlattened = me.flattenToString(); + + String existingListeners = Settings.Secure.getString(context.getContentResolver(), + Settings.Secure.ENABLED_NOTIFICATION_LISTENERS); + + if (!TextUtils.isEmpty(existingListeners)) { + if (existingListeners.contains(meFlattened)) { + return; + } else { + existingListeners += ":" + meFlattened; + } + } else { + existingListeners = meFlattened; + } + + Settings.Secure.putString(context.getContentResolver(), + Settings.Secure.ENABLED_NOTIFICATION_LISTENERS, + existingListeners); + } +}
\ No newline at end of file diff --git a/src/com/cyngn/theme/chooser/PagerContainer.java b/src/com/cyngn/theme/chooser/PagerContainer.java new file mode 100644 index 0000000..085ea68 --- /dev/null +++ b/src/com/cyngn/theme/chooser/PagerContainer.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2012 Wireless Designs, LLC + * Portions copyright (C) 2014, The CyanogenMod Project + * + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.cyngn.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/com/cyngn/theme/chooser/ThemeFragment.java b/src/com/cyngn/theme/chooser/ThemeFragment.java new file mode 100644 index 0000000..f6823ea --- /dev/null +++ b/src/com/cyngn/theme/chooser/ThemeFragment.java @@ -0,0 +1,1902 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.theme.chooser; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.IntEvaluator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.app.WallpaperManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ThemeUtils; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.ThemeConfig; +import android.content.res.ThemeManager; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +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.Bundle; +import android.os.FileUtils; +import android.os.Handler; +import android.provider.Settings; +import android.provider.ThemesContract; +import android.provider.ThemesContract.PreviewColumns; +import android.provider.ThemesContract.ThemesColumns; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.util.Log; +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.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.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import com.cyngn.theme.chooser.ComponentSelector.OnItemClickedListener; +import com.cyngn.theme.util.AudioUtils; +import com.cyngn.theme.util.BootAnimationHelper; +import com.cyngn.theme.util.IconPreviewHelper; +import com.cyngn.theme.util.ThemedTypefaceHelper; +import com.cyngn.theme.util.TypefaceHelperCache; +import com.cyngn.theme.util.Utils; +import com.cyngn.theme.widget.BootAniImageView; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipFile; + +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_ALARMS; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_BOOT_ANIM; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_LAUNCHER; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_LOCKSCREEN; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_NOTIFICATIONS; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_OVERLAYS; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_RINGTONES; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_STATUS_BAR; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_NAVIGATION_BAR; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_ICONS; +import static android.provider.ThemesContract.ThemesColumns.MODIFIES_FONTS; + +public class ThemeFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>, + ThemeManager.ThemeChangeListener { + 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 REQUEST_UNINSTALL = 1; // Request code + + private static final String NAVIGATION_BAR_BACKGROUND = "navbar_background"; + + 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_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 int LOADER_ID_ALL = 0; + private static final int LOADER_ID_STATUS_BAR = 1; + private static final int LOADER_ID_FONT = 2; + private static final int LOADER_ID_ICONS = 3; + private static final int LOADER_ID_WALLPAPER = 4; + private static final int LOADER_ID_NAVIGATION_BAR = 5; + private static final int LOADER_ID_LOCKSCREEN = 6; + private static final int LOADER_ID_STYLE = 7; + private static final int LOADER_ID_BOOT_ANIMATION = 8; + private static final int LOADER_ID_RINGTONE = 9; + private static final int LOADER_ID_NOTIFICATION = 10; + private static final int LOADER_ID_ALARM = 11; + + private static ComponentName[] sIconComponents; + + private static TypefaceHelperCache sTypefaceHelperCache; + + /** + * Maps the card's resource ID to a theme component + */ + private final SparseArray<String> mCardIdsToComponentTypes = new SparseArray<String>(); + + private String mPkgName; + private Typeface mTypefaceNormal; + private int mBatteryStyle; + + private ScrollView mScrollView; + private ViewGroup mScrollContent; + private ViewGroup mPreviewContent; // Contains icons, font, nav/status etc. Not wallpaper + private View mLoadingView; + + //Status Bar Views + private ImageView mBluetooth; + private ImageView mWifi; + private ImageView mSignal; + private ImageView mBattery; + private TextView mClock; + + // Other Misc Preview Views + private FrameLayout mShadowFrame; + private ImageView mWallpaper; + private ViewGroup mStatusBar; + private TextView mFontPreview; + private ViewGroup mIconContainer; + private ViewGroup mStyleContainer; + private ViewGroup mBootAnimationContainer; + private BootAniImageView mBootAnimation; + + // Nav Bar Views + private ViewGroup mNavBar; + private ImageView mBackButton; + private ImageView mHomeButton; + private ImageView mRecentButton; + + // Title Card Views + private ViewGroup mTitleCard; + private ViewGroup mTitleLayout; + private TextView mTitle; + private ImageView mApply; + private ImageView mOverflow; + private ProgressBar mProgress; + + // Additional Card Views + private LinearLayout mAdditionalCards; + private WallpaperCardView mWallpaperCard; + private WallpaperCardView mLockScreenCard; + + // Style views + private ImageView mStylePreview; + + // Sound cards + private ViewGroup mRingtoneContainer; + private ImageView mRingtonePlayPause; + private ViewGroup mNotificationContainer; + private ImageView mNotificationPlayPause; + private ViewGroup mAlarmContainer; + private ImageView mAlarmPlayPause; + private Map<ImageView, MediaPlayer> mMediaPlayers; + + private Handler mHandler; + + private boolean mIsUninstalled; + private int mActiveCardId = -1; + private ComponentSelector mSelector; + // Supported components for the theme this fragment represents + private Map<String, String> mSelectedComponentsMap = new HashMap<String, String>(); + // Current system theme configuration as component -> pkgName + private Map<String, String> mCurrentTheme = new HashMap<String, String>(); + + static ThemeFragment newInstance(String pkgName) { + if (sTypefaceHelperCache == null) { + sTypefaceHelperCache = TypefaceHelperCache.getInstance(); + } + ThemeFragment f = new ThemeFragment(); + Bundle args = new Bundle(); + args.putString("pkgName", pkgName); + 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("pkgName"); + mBatteryStyle = Settings.System.getInt(context.getContentResolver(), + Settings.System.STATUS_BAR_BATTERY, 0); + + getIconComponents(context); + ThemedTypefaceHelper helper = sTypefaceHelperCache.getHelperForTheme(context, + CURRENTLY_APPLIED_THEME.equals(mPkgName) ? getAppliedFontPackageName() : 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 = (ScrollView) v.findViewById(android.R.id.list); + mScrollContent = (ViewGroup) mScrollView.getChildAt(0); + mPreviewContent = (ViewGroup) v.findViewById(R.id.preview_container); + mLoadingView = v.findViewById(R.id.loading_view); + + // Status Bar + 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); + mFontPreview = (TextView) v.findViewById(R.id.font_preview); + mFontPreview.setTypeface(mTypefaceNormal); + mIconContainer = (ViewGroup) v.findViewById(R.id.icon_container); + mShadowFrame = (FrameLayout) v.findViewById(R.id.shadow_frame); + mStyleContainer = (ViewGroup) v.findViewById(R.id.style_card); + mStylePreview = (ImageView) v.findViewById(R.id.style_preview); + mBootAnimationContainer = (ViewGroup) v.findViewById(R.id.bootani_preview_container); + mBootAnimation = + (BootAniImageView) mBootAnimationContainer.findViewById(R.id.bootani_preview); + mRingtoneContainer = (ViewGroup) v.findViewById(R.id.ringtone_preview_container); + mRingtonePlayPause = (ImageView) mRingtoneContainer.findViewById(R.id.play_pause); + mNotificationContainer = (ViewGroup) v.findViewById(R.id.notification_preview_container); + mNotificationPlayPause = (ImageView) mNotificationContainer.findViewById(R.id.play_pause); + mAlarmContainer = (ViewGroup) v.findViewById(R.id.alarm_preview_container); + mAlarmPlayPause = (ImageView) mAlarmContainer.findViewById(R.id.play_pause); + + // Nav Bar + 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); + 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) { + PopupMenu popupmenu = new PopupMenu(getActivity(), mTitleCard, Gravity.END); + popupmenu.getMenuInflater().inflate(R.menu.overflow, popupmenu.getMenu()); + + if (CURRENTLY_APPLIED_THEME.equals(mPkgName) || + mPkgName.equals(ThemeUtils.getDefaultThemePackageName(getActivity())) || + mPkgName.equals(ThemeConfig.HOLO_DEFAULT)) { + Menu menu = popupmenu.getMenu(); + menu.findItem(R.id.menu_delete).setEnabled(false); + } + + popupmenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch(item.getItemId()) { + case R.id.menu_author: + Toast.makeText(getActivity(), + "Not supported", + Toast.LENGTH_LONG).show(); + break; + case R.id.menu_delete: + uninstallTheme(); + break; + } + + return true; + } + }); + popupmenu.show(); + } + }); + mApply = (ImageView) v.findViewById(R.id.apply); + mApply.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + applyTheme(); + } + }); + if (CURRENTLY_APPLIED_THEME.equals(mPkgName)) { + mApply.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); + + getLoaderManager().initLoader(LOADER_ID_ALL, null, this); + + initCards(v); + + return v; + } + + @Override + public void onResume() { + super.onResume(); + if (CURRENTLY_APPLIED_THEME.equals(mPkgName)) { + if (getLoaderManager().getLoader(0) != null) { + getLoaderManager().restartLoader(0, null, this); + } + } + } + + @Override + public void onPause() { + super.onPause(); + stopMediaPlayers(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + freeMediaPlayers(); + } + + @Override + public void onProgress(int progress) { + mProgress.setProgress(progress); + } + + @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) { + mProgress.setProgress(100); + animateProgressOut(); + ((ChooserActivity) getActivity()).themeChangeEnded(); + } + } + + public void expand() { + // 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); + mIconContainer.setPadding(padding.left, padding.top, padding.right, padding.bottom); + mShadowFrame.setBackground(null); + mShadowFrame.setPadding(0, 0, 0, 0); + mAdditionalCards.setPadding(padding.left, padding.top, padding.right, padding.bottom); + + // 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(); + 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 = ((ChooserActivity) getActivity()).getComponentSelector(); + mSelector.setOnItemClickedListener(mOnComponentItemClicked); + if (mBootAnimation != null) mBootAnimation.start(); + } + + + + // 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 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) { + // 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); + + //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); + 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); + + 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(); + } + } + + mScrollView.requestLayout(); + animateChildren(false, getChildrensGlobalBounds(mPreviewContent)); + animateExtras(false); + animateWallpaperIn(); + animateTitleCard(false, applyTheme); + if (mBootAnimation != null) mBootAnimation.stop(); + stopMediaPlayers(); + } + + // 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); + + // 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; + v.setTranslationY((prevY - endY - paddingTop) + (prevHeight - endHeight) / 2); + 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("system_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 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); + if (expand) { + mTitleCard.setVisibility(View.GONE); + } else if (applyTheme) { + // since the title card is the last animation when collapsing + // 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); + } + + private String getAppliedFontPackageName() { + final Configuration config = getActivity().getResources().getConfiguration(); + final ThemeConfig themeConfig = config != null ? config.themeConfig : null; + return themeConfig != null ? themeConfig.getFontPkgName() : + ThemeConfig.getSystemTheme().getFontPkgName(); + } + + private ThemeManager getThemeManager() { + final Context context = getActivity(); + if (context != null) { + return (ThemeManager) context.getSystemService(Context.THEME_SERVICE); + } + return null; + } + + private void freeMediaPlayers() { + for (MediaPlayer mp : mMediaPlayers.values()) { + if (mp != null) { + mp.stop(); + mp.release(); + } + } + mMediaPlayers.clear(); + } + + private View.OnClickListener mPlayPauseClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + MediaPlayer mp = (MediaPlayer) v.getTag(); + if (mp != null) { + if (mp.isPlaying()) { + ((ImageView) v).setImageResource(R.drawable.media_sound_preview); + mp.pause(); + mp.seekTo(0); + } else { + stopMediaPlayers(); + ((ImageView) v).setImageResource(R.drawable.media_sound_stop); + mp.start(); + } + } + } + }; + + private 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); + } + } + } + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + String pkgName = mPkgName; + if (args != null) { + pkgName = args.getString("pkgName"); + } + Uri uri = ThemesContract.PreviewColumns.CONTENT_URI; + String selection = ThemesContract.ThemesColumns.PKG_NAME + "= ?"; + String[] selectionArgs = new String[] { pkgName }; + String[] projection = null; + switch (id) { + case LOADER_ID_ALL: + if (!CURRENTLY_APPLIED_THEME.equals(pkgName)) { + projection = new String[] { + ThemesColumns.PKG_NAME, + ThemesColumns.TITLE, + ThemesColumns.WALLPAPER_URI, + ThemesColumns.HOMESCREEN_URI, + // 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 + }; + } else { + 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, + // TODO: add this to the ThemesContract if this + // design moves beyond prototype + NAVIGATION_BAR_BACKGROUND + }; + uri = PreviewColumns.APPLIED_URI; + selection = null; + selectionArgs = null; + } + 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: + 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, + PreviewColumns.ICON_PREVIEW_4 + }; + break; + case LOADER_ID_WALLPAPER: + 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_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(getActivity(), uri, projection, selection, selectionArgs, null); + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor c) { + c.moveToFirst(); + if (c.getCount() == 0) return; + switch (loader.getId()) { + case LOADER_ID_ALL: + populateSupportedComponents(c); + loadStatusBar(c, false); + loadIcons(c, false); + loadNavBar(c, false); + loadTitle(c); + loadFont(c, false); + loadAndRemoveAdditionalCards(c); + mHandler.post(new Runnable() { + @Override + public void run() { + animateContentIn(); + } + }); + break; + case LOADER_ID_STATUS_BAR: + loadStatusBar(c, true); + break; + case LOADER_ID_FONT: + loadFont(c, true); + break; + case LOADER_ID_ICONS: + loadIcons(c, true); + break; + case LOADER_ID_WALLPAPER: + loadWallpaper(c, true); + break; + case LOADER_ID_NAVIGATION_BAR: + loadNavBar(c, true); + break; + case LOADER_ID_LOCKSCREEN: + loadLockScreen(c, true); + break; + case LOADER_ID_STYLE: + loadStyle(c, true); + break; + case LOADER_ID_BOOT_ANIMATION: + loadBootAnimation(c, true); + break; + case LOADER_ID_RINGTONE: + loadAudible(RingtoneManager.TYPE_RINGTONE, c, true); + break; + case LOADER_ID_NOTIFICATION: + loadAudible(RingtoneManager.TYPE_NOTIFICATION, c, true); + break; + case LOADER_ID_ALARM: + loadAudible(RingtoneManager.TYPE_ALARM, c, true); + break; + } + } + + @Override + public void onLoaderReset(Loader<Cursor> loader) {} + + private void loadAndRemoveAdditionalCards(Cursor c) { + LinkedList<View> removeList = new LinkedList<View>(); + for(int i=0; i < mAdditionalCards.getChildCount(); i++) { + View v = mAdditionalCards.getChildAt(i); + if (v instanceof ComponentCardView) { + String component = mCardIdsToComponentTypes.get(v.getId()); + if (shouldShowComponentCard(component)) { + loadAdditionalCard(c, component); + } else { + removeList.add(v); + // remove the Space below this card + removeList.add(mAdditionalCards.getChildAt(i+1)); + + if (component != null && !shouldShowComponentCard(component)) { + mCardIdsToComponentTypes.remove(v.getId()); + } + } + } + } + + // TODO: It might make more sense to not inflate so many views if we do not + // plan to actually use them. But it is nice to be able to declare them in the + // xml layout under additionalCards + for(View v : removeList) { + mAdditionalCards.removeView(v); + } + + if (shouldShowComponentCard(MODIFIES_LOCKSCREEN)) { + loadLockScreen(c, false); + } else { + mAdditionalCards.removeView(mLockScreenCard); + } + } + + private void loadAdditionalCard(Cursor c, String component) { + if (MODIFIES_LOCKSCREEN.equals(component)) { + loadLockScreen(c, false); + } else if (MODIFIES_LAUNCHER.equals(component)) { + loadWallpaper(c, false); + } else if (MODIFIES_OVERLAYS.equals(component)) { + loadStyle(c, false); + } else if (MODIFIES_BOOT_ANIM.equals(component)) { + loadBootAnimation(c, false); + } else if (MODIFIES_RINGTONES.equals(component)) { + loadAudible(RingtoneManager.TYPE_RINGTONE, c, false); + } else if (MODIFIES_NOTIFICATIONS.equals(component)) { + loadAudible(RingtoneManager.TYPE_NOTIFICATION, c, false); + } else if (MODIFIES_ALARMS.equals(component)) { + loadAudible(RingtoneManager.TYPE_ALARM, c, false); + } else { + throw new IllegalArgumentException("Don't know how to load: " + component); + } + } + + private void populateSupportedComponents(Cursor c) { + // Currently applied theme doesn't return the columns we expect and + // it doesn't matter because currently applied theme shows everything anyhow + if (CURRENTLY_APPLIED_THEME.equals(mPkgName)) { + return; + } + + 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 = c.getInt(modifiesCompIdx) == 1; + if (supported) { + mSelectedComponentsMap.put(component, pkg); + } + } + if (!mSelectedComponentsMap.containsKey(MODIFIES_BOOT_ANIM)) { + mBootAnimation = null; + } + } + + /** + * 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 + * + */ + private Boolean shouldShowComponentCard(String component) { + if (CURRENTLY_APPLIED_THEME.equals(mPkgName)) { + return true; + } + String pkg = mSelectedComponentsMap.get(component); + return pkg != null && pkg.equals(mPkgName); + } + + private void loadTitle(Cursor c) { + if (CURRENTLY_APPLIED_THEME.equals(mPkgName)) { + mTitle.setText(R.string.my_theme); + } else { + int titleIdx = c.getColumnIndex(ThemesColumns.TITLE); + String title = c.getString(titleIdx); + mTitle.setText(title); + } + } + + private void loadWallpaper(Cursor c, boolean animate) { + Drawable overlay = null; + if (animate) { + overlay = getOverlayDrawable(mWallpaperCard, true); + } + + int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME); + int wpIdx = c.getColumnIndex(PreviewColumns.WALLPAPER_PREVIEW); + final Resources res = getResources(); + if (pkgNameIdx > -1) { + Bitmap bitmap = Utils.loadBitmapBlob(c, wpIdx); + mWallpaper.setImageBitmap(bitmap); + mWallpaperCard.setWallpaper(new BitmapDrawable(res, bitmap)); + String pkgName = c.getString(pkgNameIdx); + mSelectedComponentsMap.put(MODIFIES_LAUNCHER, pkgName); + } else { + final Context context = getActivity(); + Drawable wp = context == null ? null : + WallpaperManager.getInstance(context).getDrawable(); + if (wp == null) { + wp = new BitmapDrawable(res, Utils.loadBitmapBlob(c, wpIdx)); + } + mWallpaper.setImageDrawable(wp); + mWallpaperCard.setWallpaper(wp); + } + + if (animate) { + animateContentChange(R.id.wallpaper_card, mWallpaperCard, overlay); + } + } + + public void loadLockScreen(Cursor c, boolean animate) { + Drawable overlay = null; + if (animate) { + overlay = getOverlayDrawable(mLockScreenCard, true); + } + + int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME); + int wpIdx = c.getColumnIndex(PreviewColumns.LOCK_WALLPAPER_PREVIEW); + final Resources res = getResources(); + if (pkgNameIdx > -1) { + Bitmap bitmap = Utils.loadBitmapBlob(c, wpIdx); + mLockScreenCard.setWallpaper(new BitmapDrawable(res, bitmap)); + String pkgName = c.getString(pkgNameIdx); + mSelectedComponentsMap.put(MODIFIES_LOCKSCREEN, pkgName); + } else { + final Context context = getActivity(); + Drawable wp = context == null ? null : + WallpaperManager.getInstance(context).getFastKeyguardDrawable(); + if (wp == null) { + wp = new BitmapDrawable(res, Utils.loadBitmapBlob(c, wpIdx)); + } + mLockScreenCard.setWallpaper(wp); + } + + if (animate) { + animateContentChange(R.id.lockscreen_card, mLockScreenCard, overlay); + } + } + + private 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 = c.getInt(wifiMarginIdx); + int clockTextColor = c.getInt(clockColorIdx); + + Drawable overlay = null; + if (animate) { + overlay = getOverlayDrawable(mStatusBar, 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); + mSelectedComponentsMap.put(MODIFIES_STATUS_BAR, pkgName); + } + if (animate) { + animateContentChange(R.id.status_bar_container, mStatusBar, overlay); + } + } + + private void loadIcons(Cursor c, boolean animate) { + int[] iconIdx = new int[4]; + iconIdx[0] = c.getColumnIndex(PreviewColumns.ICON_PREVIEW_1); + iconIdx[1] = c.getColumnIndex(PreviewColumns.ICON_PREVIEW_2); + iconIdx[2] = c.getColumnIndex(PreviewColumns.ICON_PREVIEW_3); + iconIdx[3] = c.getColumnIndex(PreviewColumns.ICON_PREVIEW_4); + 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(), ""); + int numOfChildren = ((ViewGroup)mIconContainer.getChildAt(1)).getChildCount(); + + List<ImageView> iconViews = new ArrayList<ImageView>(numOfChildren); + for(int i=0; i < numOfChildren; i++) { + final View view = (View) ((ViewGroup)mIconContainer.getChildAt(1)).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); + mSelectedComponentsMap.put(MODIFIES_ICONS, pkgName); + } + } + + private 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(NAVIGATION_BAR_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); + } + + mNavBar.setBackground(new BitmapDrawable(getActivity().getResources(), background)); + mBackButton.setImageBitmap(backButton); + mHomeButton.setImageBitmap(homeButton); + mRecentButton.setImageBitmap(recentButton); + + if (pkgNameIdx > -1) { + String pkgName = c.getString(pkgNameIdx); + mSelectedComponentsMap.put(MODIFIES_NAVIGATION_BAR, pkgName); + } + if (animate) { + animateContentChange(R.id.navigation_bar_container, mNavBar, overlay); + } + } + + private void loadFont(Cursor c, boolean animate) { + Drawable overlay = null; + if (animate) { + overlay = getOverlayDrawable(mFontPreview, true); + } + + int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME); + String pkgName = pkgNameIdx >= 0 ? c.getString(pkgNameIdx) : mPkgName; + TypefaceHelperCache cache = TypefaceHelperCache.getInstance(); + ThemedTypefaceHelper helper = cache.getHelperForTheme(getActivity(), + CURRENTLY_APPLIED_THEME.equals(pkgName) ? getAppliedFontPackageName() : pkgName); + mTypefaceNormal = helper.getTypeface(Typeface.NORMAL); + mFontPreview.setTypeface(mTypefaceNormal); + if (pkgNameIdx > -1) { + mSelectedComponentsMap.put(MODIFIES_FONTS, pkgName); + } + if (animate) { + animateContentChange(R.id.font_preview_container, mFontPreview, overlay); + } + } + + private void loadStyle(Cursor c, boolean animate) { + Drawable overlay = null; + if (animate) { + overlay = getOverlayDrawable(mStylePreview, true); + } + + 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); + mSelectedComponentsMap.put(MODIFIES_OVERLAYS, pkgName); + } + if (animate) { + animateContentChange(R.id.style_card, mStylePreview, overlay); + } + } + + private void loadBootAnimation(Cursor c, boolean autoStart) { + int pkgNameIdx = c.getColumnIndex(ThemesColumns.PKG_NAME); + if (mBootAnimation != null) { + String pkgName; + if (pkgNameIdx > -1) { + pkgName = c.getString(pkgNameIdx); + mSelectedComponentsMap.put(MODIFIES_BOOT_ANIM, pkgName); + } else { + pkgName = mCurrentTheme.get(MODIFIES_BOOT_ANIM); + } + mBootAnimation.stop(); + new AnimationLoader(getActivity(), pkgName, mBootAnimation, autoStart).execute(); + } + } + + private void loadAudible(int type, Cursor c, boolean animate) { + View audibleContainer = null; + ImageView playPause = null; + String component = null; + switch (type) { + case RingtoneManager.TYPE_RINGTONE: + audibleContainer = mRingtoneContainer; + playPause = mRingtonePlayPause; + component = MODIFIES_RINGTONES; + break; + case RingtoneManager.TYPE_NOTIFICATION: + audibleContainer = mNotificationContainer; + playPause = mNotificationPlayPause; + component = MODIFIES_NOTIFICATIONS; + break; + case RingtoneManager.TYPE_ALARM: + audibleContainer = mAlarmContainer; + playPause = mAlarmPlayPause; + component = MODIFIES_ALARMS; + break; + } + if (audibleContainer == null) return; + TextView label = (TextView) audibleContainer.findViewById(R.id.label); + label.setText(getAudibleLabel(type)); + + 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(); + } + if (pkgNameIdx > -1) { + String pkgName = c.getString(pkgNameIdx); + try { + AudioUtils.loadThemeAudible(getActivity(), type, pkgName, mp); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Unable to load sound for " + pkgName, e); + return; + } + title.setText(c.getString(titleIdx)); + mSelectedComponentsMap.put(component, pkgName); + } else { + final Context context = getActivity(); + Uri ringtoneUri; + try { + ringtoneUri = AudioUtils.loadDefaultAudible(context, type, mp); + } catch (IOException e) { + Log.w(TAG, "Unable to load default sound ", e); + return; + } + title.setText(RingtoneManager.getRingtone(context, ringtoneUri).getTitle(context)); + } + + playPause.setTag(mp); + mMediaPlayers.put(playPause, mp); + playPause.setOnClickListener(mPlayPauseClickListener); + mp.setOnCompletionListener(mPlayCompletionListener); + } + + private 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; + } + + private 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; + } + + 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; + } + + 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 initCards(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 (mActiveCardId > 0) { + mActiveCardId = -1; + getActivity().onBackPressed(); + return; + } + mActiveCardId = v.getId(); + String component = mCardIdsToComponentTypes.get(mActiveCardId); + ((ChooserActivity) getActivity()).showComponentSelector(component, v); + fadeOutNonSelectedCards(mActiveCardId); + stopMediaPlayers(); + } + }; + + private OnItemClickedListener mOnComponentItemClicked = new OnItemClickedListener() { + @Override + public void onItemClicked(String pkgName) { + Bundle args = new Bundle(); + args.putString("pkgName", pkgName); + int loaderId = -1; + String component = mSelector.getComponentType(); + 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)) { + loaderId = LOADER_ID_WALLPAPER; + } else if (MODIFIES_LOCKSCREEN.equals(component)) { + 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; + } + getLoaderManager().restartLoader(loaderId, args, ThemeFragment.this); + } + }; + + 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)); + card.animateCardFadeOut(); + } + } + } + + private 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) { + if (mSelectedComponentsMap != null && mSelectedComponentsMap.size() > 0) { + if (!CURRENTLY_APPLIED_THEME.equals(mPkgName)) { + ThemeUtils.completeComponentMap(getActivity(), + mSelectedComponentsMap); + } + // Post this on mHandler so the client is added and removed from the same + // thread + mHandler.post(new Runnable() { + @Override + public void run() { + ThemeManager tm = getThemeManager(); + if (tm != null) { + // if this is not the "my theme" card, add missing components + // from defaults + tm.addClient(ThemeFragment.this); + tm.requestThemeChange(mSelectedComponentsMap); + } + } + }); + } + } + } + }; + + private void applyTheme() { + if (mSelectedComponentsMap == null || mSelectedComponentsMap.size() <= 0) return; + ((ChooserActivity) getActivity()).themeChangeStarted(); + animateProgressIn(mApplyThemeRunnable); + } + + 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 / 4)) + .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); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + + mTitleLayout.animate() + .translationXBy((pivotX / 4)) + .alpha(1f) + .setDuration(ANIMATE_TITLE_IN_DURATION) + .setInterpolator(new AccelerateInterpolator()) + .start(); + mProgress.startAnimation(scaleAnim); + } + + private void animateContentIn() { + 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(); + } + + public void fadeInCards() { + mActiveCardId = -1; + for (int i = 0; i < mCardIdsToComponentTypes.size(); i++) { + ComponentCardView card = (ComponentCardView) getView().findViewById( + mCardIdsToComponentTypes.keyAt(i)); + card.animateCardFadeIn(); + } + } + + public boolean componentsChanged() { + for (String key : mSelectedComponentsMap.keySet()) { + if (!mPkgName.equals(mSelectedComponentsMap.get(key))) { + return true; + } + } + return false; + } + + public void clearChanges() { + mSelectedComponentsMap.clear(); + getLoaderManager().restartLoader(LOADER_ID_ALL, null, ThemeFragment.this); + } + + private void uninstallTheme() { + Uri packageURI = Uri.parse("package:" + mPkgName); + Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageURI); + uninstallIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, true); + startActivityForResult(uninstallIntent, + ChooserActivity.REQUEST_UNINSTALL); + } + + public void onActivityResult (int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_UNINSTALL) { + try { + ApplicationInfo ainfo = getActivity() + .getPackageManager().getApplicationInfo(mPkgName, 0); + } catch (PackageManager.NameNotFoundException e) { + mIsUninstalled = true; + } + } + } + + public boolean isUninstalled() { + return mIsUninstalled; + } + + public void setCurrentTheme(Map<String, String> currentTheme) { + mCurrentTheme = currentTheme; + } + + class AnimationLoader extends AsyncTask<Void, Void, Boolean> { + Context mContext; + String mPkgName; + BootAniImageView mBootAnim; + boolean mAutoStart; + + public AnimationLoader(Context context, String pkgName, BootAniImageView bootAnim) { + this(context, pkgName, bootAnim, false); + } + + public AnimationLoader(Context context, String pkgName, BootAniImageView bootAnim, + boolean autoStart) { + mContext = context; + mPkgName = pkgName; + mBootAnim = bootAnim; + mAutoStart = autoStart; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + } + + @Override + protected Boolean doInBackground(Void... params) { + if (mContext == null) { + return Boolean.FALSE; + } + ZipFile zip = null; + if (ThemeConfig.HOLO_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 && mAutoStart) { + mBootAnim.start(); + } + } + } +} diff --git a/src/com/cyngn/theme/chooser/WallpaperCardView.java b/src/com/cyngn/theme/chooser/WallpaperCardView.java new file mode 100644 index 0000000..22d1a32 --- /dev/null +++ b/src/com/cyngn/theme/chooser/WallpaperCardView.java @@ -0,0 +1,115 @@ +package com.cyngn.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); + } + + @Override + public void expand(boolean showLabel) { + // Do nothing + } + + @Override + public void collapse() { + // Do nothing + } + + /** + * 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/com/cyngn/theme/util/AudioUtils.java b/src/com/cyngn/theme/util/AudioUtils.java new file mode 100644 index 0000000..d9621df --- /dev/null +++ b/src/com/cyngn/theme/util/AudioUtils.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.theme.util; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ThemeUtils; +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; +import android.content.res.ThemeConfig; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.util.Log; + +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.HOLO_DEFAULT.equals(pkgName)) { + loadSystemAudible(type, mp); + return; + } + PackageInfo pi = context.getPackageManager().getPackageInfo(pkgName, 0); + Context themeCtx = context.createPackageContext(pkgName, 0); + if (pi.isLegacyThemeApk) { + loadLegacyThemeAudible(themeCtx, type, pi, mp); + return; + } + AssetManager assetManager = themeCtx.getAssets(); + String assetPath; + switch (type) { + case RingtoneManager.TYPE_ALARM: + assetPath = "alarms"; + break; + case RingtoneManager.TYPE_NOTIFICATION: + assetPath = "notifications"; + break; + case RingtoneManager.TYPE_RINGTONE: + assetPath = "ringtones"; + break; + default: + assetPath = null; + break; + } + if (assetPath != null) { + try { + String[] assetList = assetManager.list(assetPath); + if (assetList != null && assetList.length > 0) { + AssetFileDescriptor afd = assetManager.openFd(assetPath + + File.separator + assetList[0]); + 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 loadLegacyThemeAudible(Context themeCtx, int type, PackageInfo pi, + MediaPlayer mp) { + if (pi.legacyThemeInfos == null || pi.legacyThemeInfos.length == 0) + return; + AssetManager assetManager = themeCtx.getAssets(); + String assetPath; + switch (type) { + case RingtoneManager.TYPE_NOTIFICATION: + assetPath = pi.legacyThemeInfos[0].notificationFileName; + break; + case RingtoneManager.TYPE_RINGTONE: + assetPath = pi.legacyThemeInfos[0].ringtoneFileName; + break; + default: + assetPath = null; + break; + } + if (assetPath != null) { + try { + AssetFileDescriptor afd = assetManager.openFd(assetPath); + if (mp != null) { + mp.reset(); + mp.setDataSource(afd.getFileDescriptor(), + afd.getStartOffset(), afd.getLength()); + mp.prepare(); + } + } catch (IOException e) { + Log.e(TAG, "Unable to load legacy sound for " + pi.packageName, 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); + mp.reset(); + mp.setDataSource(context, ringtoneUri); + mp.prepare(); + + return ringtoneUri; + } +} diff --git a/src/com/cyngn/theme/util/BootAnimationHelper.java b/src/com/cyngn/theme/util/BootAnimationHelper.java new file mode 100644 index 0000000..23ced7c --- /dev/null +++ b/src/com/cyngn/theme/util/BootAnimationHelper.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.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.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.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +public class BootAnimationHelper { + 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 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 context + * @param zip The bootanimation.zip + * @return A list of AnimationPart if successful, null if not. + * @throws IOException + */ + public static List<AnimationPart> parseAnimation(Context context, ZipFile zip) + throws IOException { + List<AnimationPart> animationParts = null; + + ZipEntry ze = zip.getEntry("desc.txt"); + if (ze != null) { + animationParts = parseDescription(zip.getInputStream(ze)); + } + + if (animationParts == null) return null; + + for (AnimationPart a : animationParts) { + for (Enumeration<? extends ZipEntry> e = zip.entries();e.hasMoreElements();) { + ze = e.nextElement(); + if (!ze.isDirectory() && ze.getName().contains(a.partName)) { + a.addFrame(ze.getName()); + } + } + Collections.sort(a.frames); + } + + 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 { + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + String line = reader.readLine(); + String[] details = line.split(" "); + 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) { + String[] info = line.split(" "); + if (info.length == 4 && (info[0].equals("p") || info[0].equals("c"))) { + 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.HOLO_DEFAULT.equals(path)) { + previewName = getPreviewFrameEntryName( + new FileInputStream(SYSTEM_BOOT_ANI_PATH)); + bitmap = loadPreviewFrame( + context, new FileInputStream(SYSTEM_BOOT_ANI_PATH), previewName); + } else { + final Context themeCtx = context.createPackageContext(path, 0); + previewName = getPreviewFrameEntryName( + themeCtx.getAssets().open("bootanimation/bootanimation.zip")); + bitmap = loadPreviewFrame(context, + themeCtx.getAssets().open("bootanimation/bootanimation.zip"), + previewName); + } + } catch (Exception e) { + // don't care since a null bitmap will be returned + e.printStackTrace(); + } + return bitmap; + } + + @Override + protected void onPostExecute(Bitmap result) { + if (result != null && imv != null) { + imv.setVisibility(View.VISIBLE); + imv.setImageBitmap(result); + } + } + } +} diff --git a/src/com/cyngn/theme/util/FontConfigParser.java b/src/com/cyngn/theme/util/FontConfigParser.java new file mode 100644 index 0000000..d88b4d4 --- /dev/null +++ b/src/com/cyngn/theme/util/FontConfigParser.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.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/com/cyngn/theme/util/IconPreviewHelper.java b/src/com/cyngn/theme/util/IconPreviewHelper.java new file mode 100644 index 0000000..a391c36 --- /dev/null +++ b/src/com/cyngn/theme/util/IconPreviewHelper.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.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 != null || iconInfo.iconUpon != null)) { + 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 == null && iconInfo.iconUpon == null) { + 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, 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/com/cyngn/theme/util/NotificationHelper.java b/src/com/cyngn/theme/util/NotificationHelper.java new file mode 100644 index 0000000..bd022cd --- /dev/null +++ b/src/com/cyngn/theme/util/NotificationHelper.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.theme.util; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.BitmapFactory; +import android.text.TextUtils; + +import com.cyngn.theme.chooser.ChooserActivity; +import com.cyngn.theme.chooser.R; + +public class NotificationHelper { + public static void postThemeInstalledNotification(Context context, String pkgName) { + String themeName = null; + try { + PackageInfo pi = context.getPackageManager().getPackageInfo(pkgName, 0); + if (pi.themeInfos != null && pi.themeInfos.length > 0) { + themeName = pi.themeInfos[0].name; + } else if (pi.legacyThemeInfos != null && pi.legacyThemeInfos[0] != null) { + themeName = pi.legacyThemeInfos[0].name; + } + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return; + } + if (TextUtils.isEmpty(themeName)) { + return; + } + + Intent intent = new Intent(context, ChooserActivity.class); + intent.setAction(Intent.ACTION_MAIN); + intent.putExtra("pkgName", pkgName); + PendingIntent pi = PendingIntent.getActivity(context, 0, intent, + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT); + + NotificationManager nm = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + Notification notice = new Notification.Builder(context) + .setAutoCancel(true) + .setOngoing(false) + .setContentTitle(String.format( + context.getString(R.string.theme_installed_notification_title), themeName)) + .setContentText(context.getString(R.string.theme_installed_notification_text)) + .setContentIntent(pi) + .setSmallIcon(R.drawable.ic_notifiy) + .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), + R.drawable.ic_app_themes)) + .setWhen(System.currentTimeMillis()) + .build(); + nm.notify(pkgName.hashCode(), notice); + } + + public static void cancelNotificationForPackage(Context context, String pkgName) { + NotificationManager nm = (NotificationManager) + context.getSystemService(Context.NOTIFICATION_SERVICE); + nm.cancel(pkgName.hashCode()); + } +} diff --git a/src/com/cyngn/theme/util/ThemedTypefaceHelper.java b/src/com/cyngn/theme/util/ThemedTypefaceHelper.java new file mode 100644 index 0000000..a46db90 --- /dev/null +++ b/src/com/cyngn/theme/util/ThemedTypefaceHelper.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.theme.util; + +import android.content.Context; +import android.content.pm.ThemeUtils; +import android.content.res.AssetManager; +import android.graphics.Typeface; +import android.util.Log; + +import com.cyngn.theme.util.FontConfigParser.Family; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.List; + +/** + * Assists in loading a themes font typefaces. + * Will load system default if there is a load issue + */ +public class ThemedTypefaceHelper { + private static final String TAG = ThemedTypefaceHelper.class.getName(); + private static final String FAMILY_SANS_SERIF = "sans-serif"; + private static final String FONTS_DIR = "fonts/"; + private static final String SYSTEM_FONTS_XML = "/system/etc/system_fonts.xml"; + private static final String SYSTEM_FONTS_DIR = "/system/fonts/"; + + private boolean mIsLoaded; + private Context mThemeContext; + private List<Family> mFamilies; + private Typeface[] mTypefaces = new Typeface[4]; + + public void load(Context context, String pkgName) { + try { + loadThemedFonts(context, pkgName); + return; + } catch(Exception e) { + Log.w(TAG, "Unable to parse and load themed fonts. Falling back to system fonts"); + } + + try { + loadSystemFonts(); + return; + } catch(Exception e) { + Log.e(TAG, "Parsing system fonts failed. Falling back to Typeface loaded fonts"); + + } + + // There is no reason for this to happen unless someone + // messed up the system_fonts.xml + loadDefaultFonts(); + } + + private void loadThemedFonts(Context context, String pkgName) throws Exception { + //Parse the font XML + mThemeContext = context.createPackageContext(pkgName, Context.CONTEXT_IGNORE_SECURITY); + AssetManager assetManager = mThemeContext.getAssets(); + InputStream is = assetManager.open(FONTS_DIR + ThemeUtils.FONT_XML); + mFamilies = FontConfigParser.parse(is); + + //Load the typefaces for sans-serif + Family sanSerif = getFamily(FAMILY_SANS_SERIF); + mTypefaces[Typeface.NORMAL] = loadTypeface(sanSerif, Typeface.NORMAL); + mTypefaces[Typeface.BOLD] = loadTypeface(sanSerif, Typeface.BOLD); + mTypefaces[Typeface.ITALIC] = loadTypeface(sanSerif, Typeface.ITALIC); + mTypefaces[Typeface.BOLD_ITALIC] = loadTypeface(sanSerif, Typeface.BOLD_ITALIC); + mIsLoaded = true; + } + + private void loadSystemFonts() throws Exception { + //Parse the system font XML + File file = new File(SYSTEM_FONTS_XML); + InputStream is = new FileInputStream(file); + mFamilies = FontConfigParser.parse(is); + + //Load the typefaces for sans-serif + Family sanSerif = getFamily(FAMILY_SANS_SERIF); + 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.nameset.contains(familyName)) { + return family; + } + } + throw new Exception("Unable to find " + familyName); + } + + private Typeface loadTypeface(Family family, int style) { + AssetManager assets = mThemeContext.getAssets(); + String path = FONTS_DIR + family.fileset.get(style); + return Typeface.createFromAsset(assets, path); + } + + private Typeface loadSystemTypeface(Family family, int style) { + return Typeface.createFromFile(SYSTEM_FONTS_DIR + family.fileset.get(style)); + } + + public Typeface getTypeface(int style) { + if (!mIsLoaded) throw new IllegalStateException("Helper was not loaded"); + return mTypefaces[style]; + } +} diff --git a/src/com/cyngn/theme/util/TypefaceHelperCache.java b/src/com/cyngn/theme/util/TypefaceHelperCache.java new file mode 100644 index 0000000..83661a6 --- /dev/null +++ b/src/com/cyngn/theme/util/TypefaceHelperCache.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.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/com/cyngn/theme/util/Utils.java b/src/com/cyngn/theme/util/Utils.java new file mode 100644 index 0000000..a6314bc --- /dev/null +++ b/src/com/cyngn/theme/util/Utils.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.theme.util; + +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Rect; +import android.provider.ThemesContract; +import android.util.Log; +import android.util.TypedValue; +import android.view.ViewConfiguration; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class Utils { + private static final String TAG = Utils.class.getSimpleName(); + + public static Bitmap decodeFile(String path, int reqWidth, int reqHeight) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + + // Determine insample size + opts.inJustDecodeBounds = true; + BitmapFactory.decodeFile(path, opts); + opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); + + // Decode the bitmap, regionally if necessary + Bitmap bitmap = null; + opts.inJustDecodeBounds = false; + Rect rect = getCropRectIfNecessary(opts, reqWidth, reqHeight); + try { + if (rect != null) { + BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(path, false); + // Check if we can downsample more now that we cropped + opts.inSampleSize = calculateInSampleSize(rect.width(), rect.height(), + reqWidth, reqHeight); + bitmap = decoder.decodeRegion(rect, opts); + } else { + bitmap = BitmapFactory.decodeFile(path, opts); + } + } catch (IOException e) { + Log.e(TAG, "Unable to open resource in path" + path, e); + } + return bitmap; + } + + public static Bitmap decodeResource(Resources res, int resId, int reqWidth, int reqHeight) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + + // Determine insample size + opts.inJustDecodeBounds = true; + BitmapFactory.decodeResource(res, resId, opts); + opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); + + // Decode the bitmap, regionally if necessary + Bitmap bitmap = null; + opts.inJustDecodeBounds = false; + Rect rect = getCropRectIfNecessary(opts, reqWidth, reqHeight); + + InputStream stream = null; + try { + if (rect != null) { + stream = res.openRawResource(resId, new TypedValue()); + if (stream == null) return null; + BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(stream, false); + // Check if we can downsample a little more now that we cropped + opts.inSampleSize = calculateInSampleSize(rect.width(), rect.height(), + reqWidth, reqHeight); + bitmap = decoder.decodeRegion(rect, opts); + } else { + bitmap = BitmapFactory.decodeResource(res, resId, opts); + } + } catch (IOException e) { + Log.e(TAG, "Unable to open resource " + resId, e); + } finally { + closeQuiet(stream); + } + return bitmap; + } + + + public static Bitmap getBitmapFromAsset(Context ctx, String path,int reqWidth, int reqHeight) { + if (ctx == null || path == null) + return null; + + String ASSET_BASE = "file:///android_asset/"; + path = path.substring(ASSET_BASE.length()); + + + Bitmap bitmap = null; + try { + AssetManager assets = ctx.getAssets(); + InputStream is = assets.open(path); + + // Determine insample size + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, opts); + opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); + is.close(); + + // Decode the bitmap, regionally if neccessary + is = assets.open(path); + opts.inJustDecodeBounds = false; + Rect rect = getCropRectIfNecessary(opts, reqWidth, reqHeight); + if (rect != null) { + BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); + // Check if we can downsample a little more now that we cropped + opts.inSampleSize = calculateInSampleSize(rect.width(), rect.height(), + reqWidth, reqHeight); + bitmap = decoder.decodeRegion(rect, opts); + } else { + bitmap = BitmapFactory.decodeStream(is); + } + } catch (IOException e) { + e.printStackTrace(); + } + return bitmap; + } + + + /** + * For excessively large images with an awkward ratio we + * will want to crop them + * @return + */ + public static Rect getCropRectIfNecessary( + BitmapFactory.Options options,int reqWidth, int reqHeight) { + Rect rect = null; + // Determine downsampled size + int width = options.outWidth / options.inSampleSize; + int height = options.outHeight / options.inSampleSize; + + if ((reqHeight * 1.5 < height)) { + int bottom = height/ 4; + int top = bottom + height/2; + rect = new Rect(0, bottom, width, top); + } else if ((reqWidth * 1.5 < width)) { + int left = width / 4; + int right = left + height/2; + rect = new Rect(left, 0, right, height); + } + return rect; + } + + public static int calculateInSampleSize( + BitmapFactory.Options options, int reqWidth, int reqHeight) { + return calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight); + } + + // Modified from original source: + // http://developer.android.com/training/displaying-bitmaps/load-bitmap.html + public static int calculateInSampleSize( + int decodeWidth, int decodeHeight, int reqWidth, int reqHeight) { + // Raw height and width of image + int inSampleSize = 1; + + if (decodeHeight > reqHeight || decodeWidth > reqWidth) { + final int halfHeight = decodeHeight / 2; + final int halfWidth = decodeWidth / 2; + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while ((halfHeight / inSampleSize) > reqHeight && + (halfWidth / inSampleSize) > reqWidth) { + inSampleSize *= 2; + } + } + + return inSampleSize; + } + + public static InputStream getInputStreamFromAsset( + Context ctx, String path) throws IOException { + if (ctx == null || path == null) + return null; + InputStream is = null; + String ASSET_BASE = "file:///android_asset/"; + path = path.substring(ASSET_BASE.length()); + AssetManager assets = ctx.getAssets(); + is = assets.open(path); + return is; + } + + public static void copy(InputStream is, OutputStream os) throws IOException { + final byte[] bytes = new byte[4096]; + int len; + while ((len = is.read(bytes)) > 0) { + os.write(bytes, 0, len); + } + } + + public static void closeQuiet(InputStream stream) { + if (stream == null) + return; + try { + stream.close(); + } catch (IOException e) { + } + } + + public static void closeQuiet(OutputStream stream) { + if (stream == null) + return; + try { + stream.close(); + } catch (IOException e) { + } + } + + //Note: will not delete populated subdirs + public static void deleteFilesInDir(String dirPath) { + File fontDir = new File(dirPath); + File[] files = fontDir.listFiles(); + if (files != null) { + for(File file : fontDir.listFiles()) { + file.delete(); + } + } + } + + public static boolean hasNavigationBar(Context context) { + return !ViewConfiguration.get(context).hasPermanentMenuKey(); + } + + public static Bitmap loadBitmapBlob(Cursor cursor, int columnIdx) { + if (columnIdx < 0) { + Log.w(TAG, "loadBitmapBlob(): Invalid index provided, returning null"); + return null; + } + byte[] blob = cursor.getBlob(columnIdx); + if (blob == null) return null; + return BitmapFactory.decodeByteArray(blob, 0, blob.length); + } + + 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; + } + } + +} diff --git a/src/com/cyngn/theme/widget/BootAniImageView.java b/src/com/cyngn/theme/widget/BootAniImageView.java new file mode 100644 index 0000000..7c599fc --- /dev/null +++ b/src/com/cyngn/theme/widget/BootAniImageView.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.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 libcore.io.IoUtils; +import com.cyngn.theme.util.BootAnimationHelper; + +import java.io.IOException; +import java.util.List; +import java.util.zip.ZipFile; + +public class BootAniImageView extends ImageView { + private static final String TAG = BootAniImageView.class.getName(); + + 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) { + // 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(mContext, mBootAniZip); + } catch (IOException e) { + 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() { + 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 (Exception 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() { + 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/com/cyngn/theme/widget/ClickableViewPager.java b/src/com/cyngn/theme/widget/ClickableViewPager.java new file mode 100644 index 0000000..9d76673 --- /dev/null +++ b/src/com/cyngn/theme/widget/ClickableViewPager.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.theme.widget; + +import android.content.Context; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +/** + * Adds click functionality for the view pager since it not normally clickable + * and focus/clickable attributes have no impact on it. + */ +public class ClickableViewPager extends ViewPager { + private boolean mIsDragging = false; + private float mSlop; + private float mLastX; + private float mLastY; + + public ClickableViewPager(Context context) { + super(context); + initView(context); + } + + public ClickableViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + initView(context); + } + + private void initView(Context context) { + mSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + mLastX = ev.getX(); + mLastY = ev.getY(); + break; + case MotionEvent.ACTION_MOVE: + float xDist = Math.abs(mLastX - ev.getX()); + float yDist = Math.abs(mLastY - ev.getY()); + if (xDist > mSlop || yDist > mSlop) { + mIsDragging = true; + } + break; + case MotionEvent.ACTION_CANCEL: + mIsDragging = false; + break; + case MotionEvent.ACTION_UP: + if (!mIsDragging) { + performClick(); + } + mIsDragging = false; + break; + } + return super.onTouchEvent(ev); + } +}
\ No newline at end of file diff --git a/src/com/cyngn/theme/widget/FittedTextView.java b/src/com/cyngn/theme/widget/FittedTextView.java new file mode 100644 index 0000000..efbe9be --- /dev/null +++ b/src/com/cyngn/theme/widget/FittedTextView.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cyngn.theme.widget; + +import android.content.Context; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.TextView; + +/** + * Change the font size to match the measured + * textview size by width + * + */ +public class FittedTextView extends TextView { + private Paint mPaint; + + public FittedTextView(Context context) { + super(context); + mPaint = new Paint(); + } + + public FittedTextView(Context context, AttributeSet attrs) { + super(context, attrs); + mPaint = new Paint(); + } + + public FittedTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mPaint = new Paint(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final float THRESHOLD = 0.5f; + final float TARGET_WIDTH = getMeasuredWidth(); + final String text = getText().toString(); + mPaint.set(getPaint()); + + float max = 200; + float min = 2; + while(max > min) { + float size = (max+min) / 2; + mPaint.setTextSize(size); + float measuredWidth = mPaint.measureText(text); + if (Math.abs(TARGET_WIDTH - measuredWidth) <= THRESHOLD) { + break; + } else if (measuredWidth > TARGET_WIDTH) { + max = size-1; + } else { + min = size+1; + } + } + this.setTextSize(TypedValue.COMPLEX_UNIT_PX, min-1); + } +} |