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.app.ActivityOptions;
20import android.app.SearchManager;
21import android.content.ActivityNotFoundException;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.PackageManager;
26import android.content.res.Resources;
27import android.media.AudioAttributes;
28import android.os.AsyncTask;
29import android.os.Bundle;
30import android.os.UserHandle;
31import android.os.Vibrator;
32import android.provider.Settings;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.view.MotionEvent;
36import android.view.View;
37import android.widget.FrameLayout;
38import android.widget.ImageView;
39
40import com.android.systemui.statusbar.BaseStatusBar;
41import com.android.systemui.statusbar.CommandQueue;
42import com.android.systemui.statusbar.StatusBarPanel;
43import com.android.systemui.statusbar.phone.PhoneStatusBar;
44
45public class SearchPanelView extends FrameLayout implements StatusBarPanel {
46
47    private static final String TAG = "SearchPanelView";
48    private static final String ASSIST_ICON_METADATA_NAME =
49            "com.android.systemui.action_assist_icon";
50
51    private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
52            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
53            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
54            .build();
55
56    private final Context mContext;
57    private BaseStatusBar mBar;
58
59    private SearchPanelCircleView mCircle;
60    private ImageView mLogo;
61    private View mScrim;
62
63    private int mThreshold;
64    private boolean mHorizontal;
65
66    private boolean mLaunching;
67    private boolean mDragging;
68    private boolean mDraggedFarEnough;
69    private float mStartTouch;
70    private float mStartDrag;
71    private boolean mLaunchPending;
72
73    public SearchPanelView(Context context, AttributeSet attrs) {
74        this(context, attrs, 0);
75    }
76
77    public SearchPanelView(Context context, AttributeSet attrs, int defStyle) {
78        super(context, attrs, defStyle);
79        mContext = context;
80        mThreshold = context.getResources().getDimensionPixelSize(R.dimen.search_panel_threshold);
81    }
82
83    private void startAssistActivity() {
84        if (!mBar.isDeviceProvisioned()) return;
85
86        // Close Recent Apps if needed
87        mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL);
88
89        final Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
90                .getAssistIntent(mContext, true, UserHandle.USER_CURRENT);
91        if (intent == null) return;
92
93        try {
94            final ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext,
95                    R.anim.search_launch_enter, R.anim.search_launch_exit);
96            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
97            AsyncTask.execute(new Runnable() {
98                @Override
99                public void run() {
100                    mContext.startActivityAsUser(intent, opts.toBundle(),
101                            new UserHandle(UserHandle.USER_CURRENT));
102                }
103            });
104        } catch (ActivityNotFoundException e) {
105            Log.w(TAG, "Activity not found for " + intent.getAction());
106        }
107    }
108
109    @Override
110    protected void onFinishInflate() {
111        super.onFinishInflate();
112        mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
113        mCircle = (SearchPanelCircleView) findViewById(R.id.search_panel_circle);
114        mLogo = (ImageView) findViewById(R.id.search_logo);
115        mScrim = findViewById(R.id.search_panel_scrim);
116    }
117
118    private void maybeSwapSearchIcon() {
119        Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
120                .getAssistIntent(mContext, false, UserHandle.USER_CURRENT);
121        if (intent != null) {
122            ComponentName component = intent.getComponent();
123            replaceDrawable(mLogo, component, ASSIST_ICON_METADATA_NAME);
124        } else {
125            mLogo.setImageDrawable(null);
126        }
127    }
128
129    public void replaceDrawable(ImageView v, ComponentName component, String name) {
130        if (component != null) {
131            try {
132                PackageManager packageManager = mContext.getPackageManager();
133                // Look for the search icon specified in the activity meta-data
134                Bundle metaData = packageManager.getActivityInfo(
135                        component, PackageManager.GET_META_DATA).metaData;
136                if (metaData != null) {
137                    int iconResId = metaData.getInt(name);
138                    if (iconResId != 0) {
139                        Resources res = packageManager.getResourcesForActivity(component);
140                        v.setImageDrawable(res.getDrawable(iconResId));
141                        return;
142                    }
143                }
144            } catch (PackageManager.NameNotFoundException e) {
145                Log.w(TAG, "Failed to swap drawable; "
146                        + component.flattenToShortString() + " not found", e);
147            } catch (Resources.NotFoundException nfe) {
148                Log.w(TAG, "Failed to swap drawable from "
149                        + component.flattenToShortString(), nfe);
150            }
151        }
152        v.setImageDrawable(null);
153    }
154
155    @Override
156    public boolean isInContentArea(int x, int y) {
157        return true;
158    }
159
160    private void vibrate() {
161        Context context = getContext();
162        if (Settings.System.getIntForUser(context.getContentResolver(),
163                Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, UserHandle.USER_CURRENT) != 0) {
164            Resources res = context.getResources();
165            Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
166            vibrator.vibrate(res.getInteger(R.integer.config_search_panel_view_vibration_duration),
167                    VIBRATION_ATTRIBUTES);
168        }
169    }
170
171    public void show(final boolean show, boolean animate) {
172        if (show) {
173            maybeSwapSearchIcon();
174            if (getVisibility() != View.VISIBLE) {
175                setVisibility(View.VISIBLE);
176                vibrate();
177                if (animate) {
178                    startEnterAnimation();
179                } else {
180                    mScrim.setAlpha(1f);
181                }
182            }
183            setFocusable(true);
184            setFocusableInTouchMode(true);
185            requestFocus();
186        } else {
187            if (animate) {
188                startAbortAnimation();
189            } else {
190                setVisibility(View.INVISIBLE);
191            }
192        }
193    }
194
195    private void startEnterAnimation() {
196        mCircle.startEnterAnimation();
197        mScrim.setAlpha(0f);
198        mScrim.animate()
199                .alpha(1f)
200                .setDuration(300)
201                .setStartDelay(50)
202                .setInterpolator(PhoneStatusBar.ALPHA_IN)
203                .start();
204
205    }
206
207    private void startAbortAnimation() {
208        mCircle.startAbortAnimation(new Runnable() {
209                    @Override
210                    public void run() {
211                        mCircle.setAnimatingOut(false);
212                        setVisibility(View.INVISIBLE);
213                    }
214                });
215        mCircle.setAnimatingOut(true);
216        mScrim.animate()
217                .alpha(0f)
218                .setDuration(300)
219                .setStartDelay(0)
220                .setInterpolator(PhoneStatusBar.ALPHA_OUT);
221    }
222
223    public void hide(boolean animate) {
224        if (mBar != null) {
225            // This will indirectly cause show(false, ...) to get called
226            mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
227        } else {
228            if (animate) {
229                startAbortAnimation();
230            } else {
231                setVisibility(View.INVISIBLE);
232            }
233        }
234    }
235
236    @Override
237    public boolean dispatchHoverEvent(MotionEvent event) {
238        // Ignore hover events outside of this panel bounds since such events
239        // generate spurious accessibility events with the panel content when
240        // tapping outside of it, thus confusing the user.
241        final int x = (int) event.getX();
242        final int y = (int) event.getY();
243        if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) {
244            return super.dispatchHoverEvent(event);
245        }
246        return true;
247    }
248
249    /**
250     * Whether the panel is showing, or, if it's animating, whether it will be
251     * when the animation is done.
252     */
253    public boolean isShowing() {
254        return getVisibility() == View.VISIBLE && !mCircle.isAnimatingOut();
255    }
256
257    public void setBar(BaseStatusBar bar) {
258        mBar = bar;
259    }
260
261    public boolean isAssistantAvailable() {
262        return ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
263                .getAssistIntent(mContext, false, UserHandle.USER_CURRENT) != null;
264    }
265
266    @Override
267    public boolean onTouchEvent(MotionEvent event) {
268        if (mLaunching || mLaunchPending) {
269            return false;
270        }
271        int action = event.getActionMasked();
272        switch (action) {
273            case MotionEvent.ACTION_DOWN:
274                mStartTouch = mHorizontal ? event.getX() : event.getY();
275                mDragging = false;
276                mDraggedFarEnough = false;
277                mCircle.reset();
278                break;
279            case MotionEvent.ACTION_MOVE:
280                float currentTouch = mHorizontal ? event.getX() : event.getY();
281                if (getVisibility() == View.VISIBLE && !mDragging &&
282                        (!mCircle.isAnimationRunning(true /* enterAnimation */)
283                                || Math.abs(mStartTouch - currentTouch) > mThreshold)) {
284                    mStartDrag = currentTouch;
285                    mDragging = true;
286                }
287                if (mDragging) {
288                    float offset = Math.max(mStartDrag - currentTouch, 0.0f);
289                    mCircle.setDragDistance(offset);
290                    mDraggedFarEnough = Math.abs(mStartTouch - currentTouch) > mThreshold;
291                    mCircle.setDraggedFarEnough(mDraggedFarEnough);
292                }
293                break;
294            case MotionEvent.ACTION_UP:
295            case MotionEvent.ACTION_CANCEL:
296                if (mDraggedFarEnough) {
297                    if (mCircle.isAnimationRunning(true  /* enterAnimation */)) {
298                        mLaunchPending = true;
299                        mCircle.setAnimatingOut(true);
300                        mCircle.performOnAnimationFinished(new Runnable() {
301                            @Override
302                            public void run() {
303                                startExitAnimation();
304                            }
305                        });
306                    } else {
307                        startExitAnimation();
308                    }
309                } else {
310                    startAbortAnimation();
311                }
312                break;
313        }
314        return true;
315    }
316
317    private void startExitAnimation() {
318        mLaunchPending = false;
319        if (mLaunching || getVisibility() != View.VISIBLE) {
320            return;
321        }
322        mLaunching = true;
323        startAssistActivity();
324        vibrate();
325        mCircle.setAnimatingOut(true);
326        mCircle.startExitAnimation(new Runnable() {
327                    @Override
328                    public void run() {
329                        mLaunching = false;
330                        mCircle.setAnimatingOut(false);
331                        setVisibility(View.INVISIBLE);
332                    }
333                });
334        mScrim.animate()
335                .alpha(0f)
336                .setDuration(300)
337                .setStartDelay(0)
338                .setInterpolator(PhoneStatusBar.ALPHA_OUT);
339    }
340
341    public void setHorizontal(boolean horizontal) {
342        mHorizontal = horizontal;
343        mCircle.setHorizontal(horizontal);
344    }
345}
346