blob: 7d0ca14cc4f14ec5046018f3fafe93794e6052df [file] [log] [blame]
/*
* Copyright (C) 2012 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 com.android.systemui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.app.ActivityOptions;
import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.media.AudioAttributes;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.Vibrator;
import android.provider.Settings;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.android.systemui.statusbar.BaseStatusBar;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarPanel;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
public class SearchPanelView extends FrameLayout implements StatusBarPanel {
private static final String TAG = "SearchPanelView";
private static final String ASSIST_ICON_METADATA_NAME =
"com.android.systemui.action_assist_icon";
private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
.build();
private final Context mContext;
private BaseStatusBar mBar;
private View mCard;
private ImageView mLogo;
private View mScrim;
private int mPeekHeight;
private int mThreshold;
private boolean mHorizontal;
private final Interpolator mLinearOutSlowInInterpolator;
private final Interpolator mFastOutLinearInInterpolator;
private boolean mAnimatingIn;
private boolean mAnimatingOut;
private boolean mDragging;
private boolean mDraggedFarEnough;
private float mStartTouch;
private float mStartDrag;
private ObjectAnimator mEnterAnimator;
private boolean mStartExitAfterAnimatingIn;
public SearchPanelView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SearchPanelView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
mPeekHeight = context.getResources().getDimensionPixelSize(R.dimen.search_card_peek_height);
mThreshold = context.getResources().getDimensionPixelSize(R.dimen.search_panel_threshold);
mLinearOutSlowInInterpolator =
AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
mFastOutLinearInInterpolator =
AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_linear_in);
}
private void startAssistActivity() {
if (!mBar.isDeviceProvisioned()) return;
// Close Recent Apps if needed
mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL);
final Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
.getAssistIntent(mContext, true, UserHandle.USER_CURRENT);
if (intent == null) return;
try {
final ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext,
R.anim.search_launch_enter, R.anim.search_launch_exit);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
AsyncTask.execute(new Runnable() {
@Override
public void run() {
mContext.startActivityAsUser(intent, opts.toBundle(),
new UserHandle(UserHandle.USER_CURRENT));
}
});
} catch (ActivityNotFoundException e) {
Log.w(TAG, "Activity not found for " + intent.getAction());
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mCard = findViewById(R.id.search_panel_card);
mLogo = (ImageView) findViewById(R.id.search_logo);
mScrim = findViewById(R.id.search_panel_scrim);
}
private void maybeSwapSearchIcon() {
Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
.getAssistIntent(mContext, false, UserHandle.USER_CURRENT);
if (intent != null) {
ComponentName component = intent.getComponent();
replaceDrawable(mLogo, component, ASSIST_ICON_METADATA_NAME);
} else {
mLogo.setImageDrawable(null);
}
}
public void replaceDrawable(ImageView v, ComponentName component, String name) {
if (component != null) {
try {
PackageManager packageManager = mContext.getPackageManager();
// Look for the search icon specified in the activity meta-data
Bundle metaData = packageManager.getActivityInfo(
component, PackageManager.GET_META_DATA).metaData;
if (metaData != null) {
int iconResId = metaData.getInt(name);
if (iconResId != 0) {
Resources res = packageManager.getResourcesForActivity(component);
v.setImageDrawable(res.getDrawable(iconResId));
return;
}
}
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Failed to swap drawable; "
+ component.flattenToShortString() + " not found", e);
} catch (Resources.NotFoundException nfe) {
Log.w(TAG, "Failed to swap drawable from "
+ component.flattenToShortString(), nfe);
}
}
v.setImageDrawable(null);
}
private boolean pointInside(int x, int y, View v) {
final int l = v.getLeft();
final int r = v.getRight();
final int t = v.getTop();
final int b = v.getBottom();
return x >= l && x < r && y >= t && y < b;
}
public boolean isInContentArea(int x, int y) {
return pointInside(x, y, mCard);
}
private void vibrate() {
Context context = getContext();
if (Settings.System.getIntForUser(context.getContentResolver(),
Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, UserHandle.USER_CURRENT) != 0) {
Resources res = context.getResources();
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
vibrator.vibrate(res.getInteger(R.integer.config_search_panel_view_vibration_duration),
VIBRATION_ATTRIBUTES);
}
}
public void show(final boolean show, boolean animate) {
if (show) {
maybeSwapSearchIcon();
if (getVisibility() != View.VISIBLE) {
setVisibility(View.VISIBLE);
vibrate();
mCard.setAlpha(1f);
if (animate) {
startEnterAnimation();
} else {
mScrim.setAlpha(1f);
if (mHorizontal) {
mCard.setX(getWidth() - mPeekHeight);
} else {
mCard.setY(getHeight() - mPeekHeight);
}
}
}
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
} else {
if (animate) {
startAbortAnimation();
} else {
setVisibility(View.INVISIBLE);
}
}
}
private void startEnterAnimation() {
if (mHorizontal) {
mCard.setX(getWidth());
} else {
mCard.setY(getHeight());
}
mAnimatingIn = true;
mCard.animate().cancel();
mEnterAnimator = ObjectAnimator.ofFloat(mCard, mHorizontal ? View.X : View.Y,
mHorizontal ? mCard.getX() : mCard.getY(),
mHorizontal ? getWidth() - mPeekHeight : getHeight() - mPeekHeight);
mEnterAnimator.setDuration(300);
mEnterAnimator.setStartDelay(50);
mEnterAnimator.setInterpolator(mLinearOutSlowInInterpolator);
mEnterAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mEnterAnimator = null;
mAnimatingIn = false;
if (mStartExitAfterAnimatingIn) {
startExitAnimation();
}
}
});
mEnterAnimator.start();
mScrim.setAlpha(0f);
mScrim.animate()
.alpha(1f)
.setDuration(300)
.setStartDelay(50)
.setInterpolator(PhoneStatusBar.ALPHA_IN)
.start();
}
private void startAbortAnimation() {
mCard.animate().cancel();
mAnimatingOut = true;
if (mHorizontal) {
mCard.animate().x(getWidth());
} else {
mCard.animate().y(getHeight());
}
mCard.animate()
.setDuration(150)
.setInterpolator(mFastOutLinearInInterpolator)
.withEndAction(new Runnable() {
@Override
public void run() {
mAnimatingOut = false;
setVisibility(View.INVISIBLE);
}
});
mScrim.animate()
.alpha(0f)
.setDuration(150)
.setStartDelay(0)
.setInterpolator(PhoneStatusBar.ALPHA_OUT);
}
public void hide(boolean animate) {
if (mBar != null) {
// This will indirectly cause show(false, ...) to get called
mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
} else {
if (animate) {
startAbortAnimation();
} else {
setVisibility(View.INVISIBLE);
}
}
}
@Override
public boolean dispatchHoverEvent(MotionEvent event) {
// Ignore hover events outside of this panel bounds since such events
// generate spurious accessibility events with the panel content when
// tapping outside of it, thus confusing the user.
final int x = (int) event.getX();
final int y = (int) event.getY();
if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) {
return super.dispatchHoverEvent(event);
}
return true;
}
/**
* Whether the panel is showing, or, if it's animating, whether it will be
* when the animation is done.
*/
public boolean isShowing() {
return getVisibility() == View.VISIBLE && !mAnimatingOut;
}
public void setBar(BaseStatusBar bar) {
mBar = bar;
}
public boolean isAssistantAvailable() {
return ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
.getAssistIntent(mContext, false, UserHandle.USER_CURRENT) != null;
}
private float rubberband(float diff) {
return Math.signum(diff) * (float) Math.pow(Math.abs(diff), 0.8f);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
mStartTouch = mHorizontal ? event.getX() : event.getY();
mDragging = false;
mDraggedFarEnough = false;
mStartExitAfterAnimatingIn = false;
break;
case MotionEvent.ACTION_MOVE:
float currentTouch = mHorizontal ? event.getX() : event.getY();
if (getVisibility() == View.VISIBLE && !mDragging &&
(!mAnimatingIn || Math.abs(mStartTouch - currentTouch) > mThreshold)) {
mStartDrag = currentTouch;
mDragging = true;
}
if (!mDraggedFarEnough && Math.abs(mStartTouch - currentTouch) > mThreshold) {
mDraggedFarEnough = true;
}
if (mDragging) {
if (!mAnimatingIn && !mAnimatingOut) {
if (Math.abs(currentTouch - mStartDrag) > mThreshold) {
startExitAnimation();
} else {
if (mHorizontal) {
mCard.setX(getWidth() - mPeekHeight + rubberband(
currentTouch - mStartDrag));
} else {
mCard.setY(getHeight() - mPeekHeight + rubberband(
currentTouch - mStartDrag));
}
}
} else if (mAnimatingIn ) {
float diff = rubberband(currentTouch - mStartDrag);
PropertyValuesHolder[] values = mEnterAnimator.getValues();
values[0].setFloatValues(
mHorizontal ? getWidth() + diff : getHeight() + diff,
mHorizontal
? getWidth() - mPeekHeight + diff
: getHeight() - mPeekHeight + diff);
mEnterAnimator.setCurrentPlayTime(mEnterAnimator.getCurrentPlayTime());
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mDraggedFarEnough) {
if (mAnimatingIn) {
mStartExitAfterAnimatingIn = true;
} else {
startExitAnimation();
}
} else {
startAbortAnimation();
}
break;
}
return true;
}
private void startExitAnimation() {
if (mAnimatingOut || getVisibility() != View.VISIBLE) {
return;
}
if (mEnterAnimator != null) {
mEnterAnimator.cancel();
}
mAnimatingOut = true;
startAssistActivity();
vibrate();
mCard.animate()
.alpha(0f)
.withLayer()
.setDuration(250)
.setInterpolator(PhoneStatusBar.ALPHA_OUT)
.withEndAction(new Runnable() {
@Override
public void run() {
mAnimatingOut = false;
setVisibility(View.INVISIBLE);
}
});
mScrim.animate()
.alpha(0f)
.setDuration(250)
.setStartDelay(0)
.setInterpolator(PhoneStatusBar.ALPHA_OUT);
}
public void setHorizontal(boolean horizontal) {
mHorizontal = horizontal;
}
}