/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tk.wasdennnoch.androidn_ify.systemui.qs.customize;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.os.Build;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toolbar;
import android.widget.Toolbar.OnMenuItemClickListener;
import java.util.ArrayList;
import de.robv.android.xposed.XposedHelpers;
import tk.wasdennnoch.androidn_ify.R;
import tk.wasdennnoch.androidn_ify.XposedHook;
import tk.wasdennnoch.androidn_ify.extracted.systemui.qs.QSDetailClipper;
import tk.wasdennnoch.androidn_ify.misc.SafeRunnable;
import tk.wasdennnoch.androidn_ify.systemui.notifications.NotificationPanelHooks;
import tk.wasdennnoch.androidn_ify.systemui.notifications.StatusBarHeaderHooks;
import tk.wasdennnoch.androidn_ify.systemui.qs.QSTileHostHooks;
import tk.wasdennnoch.androidn_ify.systemui.qs.TileAdapter;
import tk.wasdennnoch.androidn_ify.ui.AddTileActivity;
import tk.wasdennnoch.androidn_ify.utils.ConfigUtils;
import tk.wasdennnoch.androidn_ify.utils.ResourceUtils;
import tk.wasdennnoch.androidn_ify.utils.ViewUtils;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
/**
* Allows full-screen customization of QS, through show() and hide().
* <p/>
* This adds itself to the status bar window, so it can appear on top of quick settings and
* *someday* do fancy animations to get into/out of it.
*/
public class QSCustomizer extends LinearLayout implements OnMenuItemClickListener, View.OnClickListener {
private static final int MENU_ADD_BROADCAST_TILE = 1;
private static final int MENU_RESET = 2;
private static final int MENU_SECURE_TILES = 3;
private static final int MENU_CM_SETTINGS = 4;
public static final int MODE_NORMAL = 0;
public static final int MODE_EDIT_SECURE = 1;
public static final int MODE_CM_SETTINGS = 2;
private final Context mContext;
private final QSDetailClipper mClipper;
private final Point mSizePoint = new Point();
private final int mColor;
private final LinearLayout mListContainer;
private final TextView mTitle;
private final String[] mTitles;
private boolean mHasNavBar;
private boolean isShown;
private final RecyclerView mRecyclerView;
private final TileAdapter mTileAdapter;
private final Toolbar mToolbar;
private final View mDoneButton;
private boolean mCustomizing;
private final int mNavigationBarSize;
private final int mNotificationPanelWidth;
private int mMode = MODE_NORMAL;
private View mCmSettings;
@SuppressWarnings("deprecation")
public QSCustomizer(Context context) {
super(context, null);
mContext = context;
Context ownContext = ResourceUtils.createOwnContext(mContext);
ResourceUtils res = ResourceUtils.getInstance(mContext);
Resources resources = mContext.getResources();
mHasNavBar = true;
mColor = resources.getColor(resources.getIdentifier("system_primary_color", "color", XposedHook.PACKAGE_SYSTEMUI));
mNavigationBarSize = resources.getDimensionPixelSize(resources.getIdentifier("navigation_bar_size", "dimen", XposedHook.PACKAGE_SYSTEMUI));
mNotificationPanelWidth = context.getResources().getIdentifier("notification_panel_width", "dimen", XposedHook.PACKAGE_SYSTEMUI);
setGravity(Gravity.CENTER_HORIZONTAL);
setOrientation(VERTICAL);
setVisibility(GONE);
setBackground(resources.getDrawable(resources.getIdentifier("qs_detail_background", "drawable", XposedHook.PACKAGE_SYSTEMUI)));
mClipper = new QSDetailClipper(this);
LayoutInflater.from(ownContext).inflate(R.layout.qs_customize_panel_content, this);
LayoutParams recyclerViewLp = new LayoutParams(MATCH_PARENT, MATCH_PARENT);
RecyclerView recyclerView = new RecyclerView(context);
recyclerView.setId(android.R.id.list);
recyclerView.setLayoutParams(recyclerViewLp);
recyclerView.setVerticalScrollBarEnabled(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
recyclerView.setScrollIndicators(SCROLL_INDICATOR_TOP);
}
mListContainer = (LinearLayout) findViewById(R.id.list_containter);
mListContainer.addView(recyclerView);
mTitle = (TextView) findViewById(R.id.title);
mTitles = new String[] {
res.getString(R.string.hide_tiles_on_lockscreen),
res.getString(R.string.cm_qs_settings)
};
mToolbar = (Toolbar) findViewById(R.id.action_bar);
TypedValue value = new TypedValue();
mContext.getTheme().resolveAttribute(android.R.attr.homeAsUpIndicator, value, true);
mToolbar.setNavigationIcon(
resources.getDrawable(value.resourceId, mContext.getTheme()));
mToolbar.setNavigationOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
hide((int) v.getX() + v.getWidth() / 2, (int) v.getY() + v.getHeight() / 2);
}
});
mToolbar.setOnMenuItemClickListener(this);
Menu menu = mToolbar.getMenu();
menu.add(Menu.NONE, MENU_RESET, 0, res.getString(R.string.reset_tiles));
if (ConfigUtils.M) {
menu.add(Menu.NONE, MENU_ADD_BROADCAST_TILE, 1, res.getString(R.string.add_custom_tile));
menu.add(Menu.NONE, MENU_SECURE_TILES, 1, res.getString(R.string.hide_tiles_on_lockscreen));
try {
mCmSettings = inflateCmSettings(context);
menu.add(Menu.NONE, MENU_CM_SETTINGS, 1, res.getString(R.string.cm_qs_settings));
} catch (Throwable ignore) {
}
}
mToolbar.setTitle(R.string.qs_edit);
mDoneButton = findViewById(R.id.done_button);
mDoneButton.setOnClickListener(this);
mRecyclerView = (RecyclerView) findViewById(android.R.id.list);
setBottomMargin(mNavigationBarSize);
mTileAdapter = new TileAdapter(mContext);
mRecyclerView.setAdapter(mTileAdapter);
mTileAdapter.getItemTouchHelper().attachToRecyclerView(mRecyclerView);
GridLayoutManager layout = new GridLayoutManager(getContext(), 3);
layout.setSpanSizeLookup(mTileAdapter.getSizeLookup());
mRecyclerView.setLayoutManager(layout);
mRecyclerView.addItemDecoration(mTileAdapter.getItemDecoration());
DefaultItemAnimator animator = new DefaultItemAnimator();
animator.setMoveDuration(TileAdapter.MOVE_DURATION);
mRecyclerView.setItemAnimator(animator);
}
private View inflateCmSettings(Context context) {
View cmSettings = View.inflate(context, context.getResources().getIdentifier("qs_settings", "layout", XposedHook.PACKAGE_SYSTEMUI), null);
LinearLayout.LayoutParams cmSettingsLp = new LayoutParams(MATCH_PARENT, MATCH_PARENT);
cmSettings.setLayoutParams(cmSettingsLp);
return cmSettings;
}
public void invalidateTileAdapter() {
mTileAdapter.invalidate();
}
public void show(ArrayList<Object> records, boolean animated) {
if (isShown) return;
initShow(records);
if (animated) {
setAlpha(0);
animate().alpha(1)
.withEndAction(mShowAnimationListener)
.start();
} else {
setAlpha(1);
}
}
public void show(ArrayList<Object> records, int x, int y) {
if (isShown) return;
initShow(records);
setAlpha(1);
mClipper.animateCircularClip(x, y, true, mExpandAnimationListener);
}
private void initShow(ArrayList<Object> records) {
if (mTileAdapter.isInvalid()) {
mTileAdapter.reInit(records, mContext);
}
setMode(MODE_NORMAL);
if (!isShown) {
isShown = true;
setVisibility(VISIBLE);
NotificationPanelHooks.addBarStateCallback(mBarStateCallback);
}
}
public boolean onBackPressed() {
if (!mCustomizing) return false;
if (mMode != MODE_NORMAL)
setMode(MODE_NORMAL);
else
hideCircular();
return true;
}
public void hide(boolean animated) {
if (!isShown) return;
saveAndHide();
if (animated) {
animate().alpha(1)
.withEndAction(mHideAnimationListener)
.start();
} else {
mHideAnimationListener.run();
}
}
public void hideCircular() {
hide(getWidth() / 2, getHeight() / 2);
}
public void hide(int x, int y) {
if (isShown) {
saveAndHide();
mClipper.animateCircularClip(x, y, false, mCollapseAnimationListener);
}
}
public void saveAndHide() {
isShown = false;
mToolbar.dismissPopupMenus();
setCustomizing(false);
save();
NotificationPanelHooks.removeBarStateCallback(mBarStateCallback);
}
private void setCustomizing(boolean customizing) {
mCustomizing = customizing;
}
public boolean isCustomizing() {
return mCustomizing;
}
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case MENU_RESET:
mTileAdapter.resetTiles();
break;
case MENU_ADD_BROADCAST_TILE:
showAddBroadcastTile();
break;
case MENU_SECURE_TILES:
setMode(MODE_EDIT_SECURE);
break;
case MENU_CM_SETTINGS:
setMode(MODE_CM_SETTINGS);
break;
}
return false;
}
private void showAddBroadcastTile() {
hide(true);
Object qsTileHost = XposedHelpers.getObjectField(StatusBarHeaderHooks.mQsPanel, "mHost");
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setClassName("tk.wasdennnoch.androidn_ify", AddTileActivity.class.getName());
intent.putExtra("color", mColor);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
try {
XposedHelpers.callMethod(qsTileHost, "startActivityDismissingKeyguard", intent);
} catch (Throwable t) {
try {
XposedHelpers.callMethod(qsTileHost, "startSettingsActivity", intent);
} catch (Throwable t2) {
XposedHook.logE("QSCustomizer", "Error starting settings activity", null);
}
}
}
private void save() {
mTileAdapter.saveChanges();
if (!ConfigUtils.M)
QSTileHostHooks.recreateTiles();
}
public void handleStateChanged(Object qsTile, Object state) {
mTileAdapter.handleStateChanged(qsTile, state);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
getDisplay().getRealSize(mSizePoint);
int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
try {
maxWidth = getResources().getDimensionPixelSize(getResources().getIdentifier("notification_panel_width", "dimen", XposedHook.PACKAGE_SYSTEMUI));
} catch (Throwable ignore) {
}
if (maxWidth == MATCH_PARENT) maxWidth = widthMeasureSpec;
super.onMeasure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(mSizePoint.y, MeasureSpec.EXACTLY));
}
private final NotificationPanelHooks.BarStateCallback mBarStateCallback = new NotificationPanelHooks.BarStateCallback() {
@Override
public void onStateChanged() {
if (NotificationPanelHooks.getStatusBarState() == NotificationPanelHooks.STATE_KEYGUARD) {
hide(false);
}
}
};
private final AnimatorListener mExpandAnimationListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
setCustomizing(true);
}
};
private final Runnable mShowAnimationListener = new Runnable() {
@Override
public void run() {
setCustomizing(true);
}
};
private final Runnable mHideAnimationListener = new Runnable() {
@Override
public void run() {
if (!isShown)
setVisibility(View.GONE);
mRecyclerView.setAdapter(mTileAdapter);
}
};
private final AnimatorListener mCollapseAnimationListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (!isShown)
setVisibility(View.GONE);
mRecyclerView.setAdapter(mTileAdapter);
}
@Override
public void onAnimationCancel(Animator animation) {
if (!isShown)
setVisibility(View.GONE);
}
};
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
int width = getResources().getDimensionPixelSize(mNotificationPanelWidth);
ViewUtils.setWidth(mRecyclerView, width);
if (!mHasNavBar) return;
boolean shouldShow = newConfig.smallestScreenWidthDp >= 600
|| newConfig.orientation != Configuration.ORIENTATION_LANDSCAPE;
setBottomMargin(shouldShow ? mNavigationBarSize : 0);
findViewById(R.id.nav_bar_background).setVisibility(shouldShow ? VISIBLE : GONE);
mRecyclerView.setAdapter(mTileAdapter);
}
private void setBottomMargin(int margin) {
ViewUtils.setMarginBottom(mRecyclerView, margin);
}
public void setHasNavBar(boolean hasNavBar) {
mHasNavBar = hasNavBar;
if (!mHasNavBar) {
setBottomMargin(0);
findViewById(R.id.nav_bar_background).setVisibility(GONE);
}
}
@Override
public void onClick(View view) {
setMode(MODE_NORMAL);
}
private void setMode(int mode) {
if (mMode == mode) return;
mMode = mode;
onModeChanged();
boolean normal = mode == MODE_NORMAL;
transition(mToolbar, normal);
transition(mDoneButton, !normal);
}
private void onModeChanged() {
mListContainer.removeAllViews();
mListContainer.addView(getCurrentView());
mTitle.setText(getTitle());
getCurrentView();
}
private String getTitle() {
switch (mMode) {
case MODE_CM_SETTINGS:
return mTitles[1];
case MODE_EDIT_SECURE:
return mTitles[0];
default:
return "";
}
}
private View getCurrentView() {
switch (mMode) {
case MODE_CM_SETTINGS:
return mCmSettings;
case MODE_EDIT_SECURE:
default:
mTileAdapter.setMode(mMode);
return mRecyclerView;
}
}
private static void transition(final View v, final boolean in) {
if (v.getVisibility() == (in ? VISIBLE : INVISIBLE)) return;
if (in) {
v.bringToFront();
v.setVisibility(View.VISIBLE);
}
if (v.hasOverlappingRendering()) {
v.animate().withLayer();
}
v.animate()
.alpha(in ? 1 : 0)
.withEndAction(new SafeRunnable() {
@Override
public void runSafe() {
if (!in) {
v.setVisibility(View.INVISIBLE);
}
}
})
.start();
}
}