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