SearchPanelView.java revision f479792e05485a536c3fa68db9d8a71f34591b78
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.PropertyValuesHolder;
23import android.app.ActivityOptions;
24import android.app.SearchManager;
25import android.content.ActivityNotFoundException;
26import android.content.ComponentName;
27import android.content.Context;
28import android.content.Intent;
29import android.content.pm.PackageManager;
30import android.content.res.Resources;
31import android.media.AudioAttributes;
32import android.os.AsyncTask;
33import android.os.Bundle;
34import android.os.UserHandle;
35import android.os.Vibrator;
36import android.provider.Settings;
37import android.util.AttributeSet;
38import android.util.Log;
39import android.view.MotionEvent;
40import android.view.View;
41import android.view.animation.AnimationUtils;
42import android.view.animation.Interpolator;
43import android.widget.FrameLayout;
44import android.widget.ImageView;
45
46import com.android.systemui.statusbar.BaseStatusBar;
47import com.android.systemui.statusbar.CommandQueue;
48import com.android.systemui.statusbar.StatusBarPanel;
49import com.android.systemui.statusbar.phone.PhoneStatusBar;
50
51public class SearchPanelView extends FrameLayout implements StatusBarPanel {
52
53    private static final String TAG = "SearchPanelView";
54    private static final String ASSIST_ICON_METADATA_NAME =
55            "com.android.systemui.action_assist_icon";
56
57    private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
58            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
59            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
60            .build();
61
62    private final Context mContext;
63    private BaseStatusBar mBar;
64
65    private View mCard;
66    private ImageView mLogo;
67    private View mScrim;
68
69    private int mPeekHeight;
70    private int mThreshold;
71    private boolean mHorizontal;
72    private final Interpolator mLinearOutSlowInInterpolator;
73    private final Interpolator mFastOutLinearInInterpolator;
74
75    private boolean mAnimatingIn;
76    private boolean mAnimatingOut;
77    private boolean mDragging;
78    private boolean mDraggedFarEnough;
79    private float mStartTouch;
80    private float mStartDrag;
81
82    private ObjectAnimator mEnterAnimator;
83
84    private boolean mStartExitAfterAnimatingIn;
85
86    public SearchPanelView(Context context, AttributeSet attrs) {
87        this(context, attrs, 0);
88    }
89
90    public SearchPanelView(Context context, AttributeSet attrs, int defStyle) {
91        super(context, attrs, defStyle);
92        mContext = context;
93        mPeekHeight = context.getResources().getDimensionPixelSize(R.dimen.search_card_peek_height);
94        mThreshold = context.getResources().getDimensionPixelSize(R.dimen.search_panel_threshold);
95        mLinearOutSlowInInterpolator =
96                AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
97        mFastOutLinearInInterpolator =
98                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_linear_in);
99    }
100
101    private void startAssistActivity() {
102        if (!mBar.isDeviceProvisioned()) return;
103
104        // Close Recent Apps if needed
105        mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL);
106
107        final Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
108                .getAssistIntent(mContext, true, UserHandle.USER_CURRENT);
109        if (intent == null) return;
110
111        try {
112            final ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext,
113                    R.anim.search_launch_enter, R.anim.search_launch_exit);
114            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
115            AsyncTask.execute(new Runnable() {
116                @Override
117                public void run() {
118                    mContext.startActivityAsUser(intent, opts.toBundle(),
119                            new UserHandle(UserHandle.USER_CURRENT));
120                }
121            });
122        } catch (ActivityNotFoundException e) {
123            Log.w(TAG, "Activity not found for " + intent.getAction());
124        }
125    }
126
127    @Override
128    protected void onFinishInflate() {
129        super.onFinishInflate();
130        mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
131        mCard = findViewById(R.id.search_panel_card);
132        mLogo = (ImageView) findViewById(R.id.search_logo);
133        mScrim = findViewById(R.id.search_panel_scrim);
134    }
135
136    private void maybeSwapSearchIcon() {
137        Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
138                .getAssistIntent(mContext, false, UserHandle.USER_CURRENT);
139        if (intent != null) {
140            ComponentName component = intent.getComponent();
141            replaceDrawable(mLogo, component, ASSIST_ICON_METADATA_NAME);
142        } else {
143            mLogo.setImageDrawable(null);
144        }
145    }
146
147    public void replaceDrawable(ImageView v, ComponentName component, String name) {
148        if (component != null) {
149            try {
150                PackageManager packageManager = mContext.getPackageManager();
151                // Look for the search icon specified in the activity meta-data
152                Bundle metaData = packageManager.getActivityInfo(
153                        component, PackageManager.GET_META_DATA).metaData;
154                if (metaData != null) {
155                    int iconResId = metaData.getInt(name);
156                    if (iconResId != 0) {
157                        Resources res = packageManager.getResourcesForActivity(component);
158                        v.setImageDrawable(res.getDrawable(iconResId));
159                        return;
160                    }
161                }
162            } catch (PackageManager.NameNotFoundException e) {
163                Log.w(TAG, "Failed to swap drawable; "
164                        + component.flattenToShortString() + " not found", e);
165            } catch (Resources.NotFoundException nfe) {
166                Log.w(TAG, "Failed to swap drawable from "
167                        + component.flattenToShortString(), nfe);
168            }
169        }
170        v.setImageDrawable(null);
171    }
172
173    private boolean pointInside(int x, int y, View v) {
174        final int l = v.getLeft();
175        final int r = v.getRight();
176        final int t = v.getTop();
177        final int b = v.getBottom();
178        return x >= l && x < r && y >= t && y < b;
179    }
180
181    public boolean isInContentArea(int x, int y) {
182        return pointInside(x, y, mCard);
183    }
184
185    private void vibrate() {
186        Context context = getContext();
187        if (Settings.System.getIntForUser(context.getContentResolver(),
188                Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, UserHandle.USER_CURRENT) != 0) {
189            Resources res = context.getResources();
190            Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
191            vibrator.vibrate(res.getInteger(R.integer.config_search_panel_view_vibration_duration),
192                    VIBRATION_ATTRIBUTES);
193        }
194    }
195
196    public void show(final boolean show, boolean animate) {
197        if (show) {
198            maybeSwapSearchIcon();
199            if (getVisibility() != View.VISIBLE) {
200                setVisibility(View.VISIBLE);
201                vibrate();
202                mCard.setAlpha(1f);
203                if (animate) {
204                    startEnterAnimation();
205                } else {
206                    mScrim.setAlpha(1f);
207                    if (mHorizontal) {
208                        mCard.setX(getWidth() - mPeekHeight);
209                    } else {
210                        mCard.setY(getHeight() - mPeekHeight);
211                    }
212                }
213            }
214            setFocusable(true);
215            setFocusableInTouchMode(true);
216            requestFocus();
217        } else {
218            if (animate) {
219                startAbortAnimation();
220            } else {
221                setVisibility(View.INVISIBLE);
222            }
223        }
224    }
225
226    private void startEnterAnimation() {
227        if (mHorizontal) {
228            mCard.setX(getWidth());
229        } else {
230            mCard.setY(getHeight());
231        }
232        mAnimatingIn = true;
233        mCard.animate().cancel();
234        mEnterAnimator = ObjectAnimator.ofFloat(mCard, mHorizontal ? View.X : View.Y,
235                mHorizontal ? mCard.getX() : mCard.getY(),
236                mHorizontal ? getWidth() - mPeekHeight : getHeight() - mPeekHeight);
237        mEnterAnimator.setDuration(300);
238        mEnterAnimator.setStartDelay(50);
239        mEnterAnimator.setInterpolator(mLinearOutSlowInInterpolator);
240        mEnterAnimator.addListener(new AnimatorListenerAdapter() {
241            @Override
242            public void onAnimationEnd(Animator animation) {
243                mEnterAnimator = null;
244                mAnimatingIn = false;
245                if (mStartExitAfterAnimatingIn) {
246                    startExitAnimation();
247                }
248            }
249        });
250        mEnterAnimator.start();
251        mScrim.setAlpha(0f);
252        mScrim.animate()
253                .alpha(1f)
254                .setDuration(300)
255                .setStartDelay(50)
256                .setInterpolator(PhoneStatusBar.ALPHA_IN)
257                .start();
258
259    }
260
261    private void startAbortAnimation() {
262        mCard.animate().cancel();
263        mAnimatingOut = true;
264        if (mHorizontal) {
265            mCard.animate().x(getWidth());
266        } else {
267            mCard.animate().y(getHeight());
268        }
269        mCard.animate()
270                .setDuration(150)
271                .setInterpolator(mFastOutLinearInInterpolator)
272                .withEndAction(new Runnable() {
273                    @Override
274                    public void run() {
275                        mAnimatingOut = false;
276                        setVisibility(View.INVISIBLE);
277                    }
278                });
279        mScrim.animate()
280                .alpha(0f)
281                .setDuration(150)
282                .setStartDelay(0)
283                .setInterpolator(PhoneStatusBar.ALPHA_OUT);
284    }
285
286    public void hide(boolean animate) {
287        if (mBar != null) {
288            // This will indirectly cause show(false, ...) to get called
289            mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
290        } else {
291            if (animate) {
292                startAbortAnimation();
293            } else {
294                setVisibility(View.INVISIBLE);
295            }
296        }
297    }
298
299    @Override
300    public boolean dispatchHoverEvent(MotionEvent event) {
301        // Ignore hover events outside of this panel bounds since such events
302        // generate spurious accessibility events with the panel content when
303        // tapping outside of it, thus confusing the user.
304        final int x = (int) event.getX();
305        final int y = (int) event.getY();
306        if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) {
307            return super.dispatchHoverEvent(event);
308        }
309        return true;
310    }
311
312    /**
313     * Whether the panel is showing, or, if it's animating, whether it will be
314     * when the animation is done.
315     */
316    public boolean isShowing() {
317        return getVisibility() == View.VISIBLE && !mAnimatingOut;
318    }
319
320    public void setBar(BaseStatusBar bar) {
321        mBar = bar;
322    }
323
324    public boolean isAssistantAvailable() {
325        return ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
326                .getAssistIntent(mContext, false, UserHandle.USER_CURRENT) != null;
327    }
328
329    private float rubberband(float diff) {
330        return Math.signum(diff) * (float) Math.pow(Math.abs(diff), 0.8f);
331    }
332
333    @Override
334    public boolean onTouchEvent(MotionEvent event) {
335        int action = event.getActionMasked();
336        switch (action) {
337            case MotionEvent.ACTION_DOWN:
338                mStartTouch = mHorizontal ? event.getX() : event.getY();
339                mDragging = false;
340                mDraggedFarEnough = false;
341                mStartExitAfterAnimatingIn = false;
342                break;
343            case MotionEvent.ACTION_MOVE:
344                float currentTouch = mHorizontal ? event.getX() : event.getY();
345                if (getVisibility() == View.VISIBLE && !mDragging &&
346                        (!mAnimatingIn || Math.abs(mStartTouch - currentTouch) > mThreshold)) {
347                    mStartDrag = currentTouch;
348                    mDragging = true;
349                }
350                if (!mDraggedFarEnough && Math.abs(mStartTouch - currentTouch) > mThreshold) {
351                    mDraggedFarEnough = true;
352                }
353                if (mDragging) {
354                    if (!mAnimatingIn && !mAnimatingOut) {
355                        if (Math.abs(currentTouch - mStartDrag) > mThreshold) {
356                            startExitAnimation();
357                        } else {
358                            if (mHorizontal) {
359                                mCard.setX(getWidth() - mPeekHeight + rubberband(
360                                        currentTouch - mStartDrag));
361                            } else {
362                                mCard.setY(getHeight() - mPeekHeight + rubberband(
363                                        currentTouch - mStartDrag));
364                            }
365                        }
366                    } else if (mAnimatingIn ) {
367                        float diff = rubberband(currentTouch - mStartDrag);
368                        PropertyValuesHolder[] values = mEnterAnimator.getValues();
369                        values[0].setFloatValues(
370                                mHorizontal ? getWidth() + diff : getHeight() + diff,
371                                mHorizontal
372                                        ? getWidth() - mPeekHeight + diff
373                                        : getHeight() - mPeekHeight + diff);
374                        mEnterAnimator.setCurrentPlayTime(mEnterAnimator.getCurrentPlayTime());
375                    }
376                }
377                break;
378            case MotionEvent.ACTION_UP:
379            case MotionEvent.ACTION_CANCEL:
380                if (mDraggedFarEnough) {
381                    if (mAnimatingIn) {
382                        mStartExitAfterAnimatingIn = true;
383                    } else {
384                        startExitAnimation();
385                    }
386                } else {
387                    startAbortAnimation();
388                }
389                break;
390        }
391        return true;
392    }
393
394    private void startExitAnimation() {
395        if (mAnimatingOut || getVisibility() != View.VISIBLE) {
396            return;
397        }
398        if (mEnterAnimator != null) {
399            mEnterAnimator.cancel();
400        }
401        mAnimatingOut = true;
402        startAssistActivity();
403        vibrate();
404        mCard.animate()
405                .alpha(0f)
406                .withLayer()
407                .setDuration(250)
408                .setInterpolator(PhoneStatusBar.ALPHA_OUT)
409                .withEndAction(new Runnable() {
410                    @Override
411                    public void run() {
412                        mAnimatingOut = false;
413                        setVisibility(View.INVISIBLE);
414                    }
415                });
416        mScrim.animate()
417                .alpha(0f)
418                .setDuration(250)
419                .setStartDelay(0)
420                .setInterpolator(PhoneStatusBar.ALPHA_OUT);
421    }
422
423    public void setHorizontal(boolean horizontal) {
424        mHorizontal = horizontal;
425    }
426}
427