Home | History | Annotate | Download | only in systemui
      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 
     17 package com.android.systemui;
     18 
     19 import android.app.ActivityOptions;
     20 import android.app.SearchManager;
     21 import android.content.ActivityNotFoundException;
     22 import android.content.ComponentName;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.pm.PackageManager;
     26 import android.content.res.Resources;
     27 import android.media.AudioAttributes;
     28 import android.os.AsyncTask;
     29 import android.os.Bundle;
     30 import android.os.UserHandle;
     31 import android.os.Vibrator;
     32 import android.provider.Settings;
     33 import android.util.AttributeSet;
     34 import android.util.Log;
     35 import android.view.MotionEvent;
     36 import android.view.View;
     37 import android.widget.FrameLayout;
     38 import android.widget.ImageView;
     39 
     40 import com.android.systemui.statusbar.BaseStatusBar;
     41 import com.android.systemui.statusbar.CommandQueue;
     42 import com.android.systemui.statusbar.StatusBarPanel;
     43 import com.android.systemui.statusbar.phone.PhoneStatusBar;
     44 
     45 public 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