1 /* 2 * Copyright (C) 2015 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.phone; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.annotation.Nullable; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ApplicationInfo; 26 import android.content.pm.PackageInfo; 27 import android.content.pm.PackageManager; 28 import android.content.pm.ResolveInfo; 29 import android.provider.Settings; 30 import android.telephony.TelephonyManager; 31 import android.text.TextUtils; 32 import android.util.AttributeSet; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.ViewAnimationUtils; 36 import android.view.ViewGroup; 37 import android.view.accessibility.AccessibilityManager; 38 import android.view.animation.AnimationUtils; 39 import android.view.animation.Interpolator; 40 import android.widget.Button; 41 import android.widget.FrameLayout; 42 import android.widget.TextView; 43 44 import java.util.List; 45 46 public class EmergencyActionGroup extends FrameLayout implements View.OnClickListener { 47 48 private static final long HIDE_DELAY = 3000; 49 private static final int RIPPLE_DURATION = 600; 50 private static final long RIPPLE_PAUSE = 1000; 51 52 private final Interpolator mFastOutLinearInInterpolator; 53 54 private ViewGroup mSelectedContainer; 55 private TextView mSelectedLabel; 56 private View mRippleView; 57 private View mLaunchHint; 58 59 private View mLastRevealed; 60 61 private MotionEvent mPendingTouchEvent; 62 63 private boolean mHiding; 64 EmergencyActionGroup(Context context, @Nullable AttributeSet attrs)65 public EmergencyActionGroup(Context context, @Nullable AttributeSet attrs) { 66 super(context, attrs); 67 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context, 68 android.R.interpolator.fast_out_linear_in); 69 } 70 71 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)72 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 73 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 74 } 75 76 @Override onFinishInflate()77 protected void onFinishInflate() { 78 super.onFinishInflate(); 79 setupAssistActions(); 80 81 mSelectedContainer = (ViewGroup) findViewById(R.id.selected_container); 82 mSelectedContainer.setOnClickListener(this); 83 mSelectedLabel = (TextView) findViewById(R.id.selected_label); 84 mRippleView = findViewById(R.id.ripple_view); 85 mLaunchHint = findViewById(R.id.launch_hint); 86 } 87 88 /** 89 * Called by the activity before a touch event is dispatched to the view hierarchy. 90 */ onPreTouchEvent(MotionEvent event)91 public void onPreTouchEvent(MotionEvent event) { 92 mPendingTouchEvent = event; 93 } 94 95 @Override dispatchTouchEvent(MotionEvent event)96 public boolean dispatchTouchEvent(MotionEvent event) { 97 boolean handled = super.dispatchTouchEvent(event); 98 if (mPendingTouchEvent == event && handled) { 99 mPendingTouchEvent = null; 100 } 101 return handled; 102 } 103 104 /** 105 * Called by the activity after a touch event is dispatched to the view hierarchy. 106 */ onPostTouchEvent(MotionEvent event)107 public void onPostTouchEvent(MotionEvent event) { 108 // Hide the confirmation button if a touch event was delivered to the activity but not to 109 // this view. 110 if (mPendingTouchEvent != null) { 111 hideTheButton(); 112 } 113 mPendingTouchEvent = null; 114 } 115 116 117 setupAssistActions()118 private void setupAssistActions() { 119 int[] buttonIds = new int[] {R.id.action1, R.id.action2, R.id.action3}; 120 121 List<ResolveInfo> infos; 122 123 if (TelephonyManager.EMERGENCY_ASSISTANCE_ENABLED) { 124 infos = resolveAssistPackageAndQueryActivites(); 125 } else { 126 infos = null; 127 } 128 129 for (int i = 0; i < 3; i++) { 130 Button button = (Button) findViewById(buttonIds[i]); 131 boolean visible = false; 132 133 button.setOnClickListener(this); 134 135 if (infos != null && infos.size() > i && infos.get(i) != null) { 136 ResolveInfo info = infos.get(i); 137 ComponentName name = getComponentName(info); 138 139 button.setTag(R.id.tag_intent, 140 new Intent(TelephonyManager.ACTION_EMERGENCY_ASSISTANCE) 141 .setComponent(name)); 142 button.setText(info.loadLabel(getContext().getPackageManager())); 143 visible = true; 144 } 145 146 button.setVisibility(visible ? View.VISIBLE : View.GONE); 147 } 148 } 149 resolveAssistPackageAndQueryActivites()150 private List<ResolveInfo> resolveAssistPackageAndQueryActivites() { 151 List<ResolveInfo> infos = queryAssistActivities(); 152 153 if (infos == null || infos.isEmpty()) { 154 PackageManager packageManager = getContext().getPackageManager(); 155 Intent queryIntent = new Intent(TelephonyManager.ACTION_EMERGENCY_ASSISTANCE); 156 infos = packageManager.queryIntentActivities(queryIntent, 0); 157 158 PackageInfo bestMatch = null; 159 for (int i = 0; i < infos.size(); i++) { 160 if (infos.get(i).activityInfo == null) continue; 161 String packageName = infos.get(i).activityInfo.packageName; 162 PackageInfo packageInfo; 163 try { 164 packageInfo = packageManager.getPackageInfo(packageName, 0); 165 } catch (PackageManager.NameNotFoundException e) { 166 continue; 167 } 168 // Get earliest installed app, but prioritize system apps. 169 if (bestMatch == null 170 || !isSystemApp(bestMatch) && isSystemApp(packageInfo) 171 || isSystemApp(bestMatch) == isSystemApp(packageInfo) 172 && bestMatch.firstInstallTime > packageInfo.firstInstallTime) { 173 bestMatch = packageInfo; 174 } 175 } 176 177 if (bestMatch != null) { 178 Settings.Secure.putString(getContext().getContentResolver(), 179 Settings.Secure.EMERGENCY_ASSISTANCE_APPLICATION, 180 bestMatch.packageName); 181 return queryAssistActivities(); 182 } else { 183 return null; 184 } 185 } else { 186 return infos; 187 } 188 } 189 queryAssistActivities()190 private List<ResolveInfo> queryAssistActivities() { 191 String assistPackage = Settings.Secure.getString( 192 getContext().getContentResolver(), 193 Settings.Secure.EMERGENCY_ASSISTANCE_APPLICATION); 194 List<ResolveInfo> infos = null; 195 196 if (!TextUtils.isEmpty(assistPackage)) { 197 Intent queryIntent = new Intent(TelephonyManager.ACTION_EMERGENCY_ASSISTANCE) 198 .setPackage(assistPackage); 199 infos = getContext().getPackageManager().queryIntentActivities(queryIntent, 0); 200 } 201 return infos; 202 } 203 isSystemApp(PackageInfo info)204 private boolean isSystemApp(PackageInfo info) { 205 return info.applicationInfo != null 206 && (info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 207 } 208 getComponentName(ResolveInfo resolveInfo)209 private ComponentName getComponentName(ResolveInfo resolveInfo) { 210 if (resolveInfo == null || resolveInfo.activityInfo == null) return null; 211 return new ComponentName(resolveInfo.activityInfo.packageName, 212 resolveInfo.activityInfo.name); 213 } 214 215 @Override onClick(View v)216 public void onClick(View v) { 217 Intent intent = (Intent) v.getTag(R.id.tag_intent); 218 219 switch (v.getId()) { 220 case R.id.action1: 221 case R.id.action2: 222 case R.id.action3: 223 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { 224 getContext().startActivity(intent); 225 } else { 226 revealTheButton(v); 227 } 228 break; 229 case R.id.selected_container: 230 if (!mHiding) { 231 getContext().startActivity(intent); 232 } 233 break; 234 } 235 } 236 revealTheButton(View v)237 private void revealTheButton(View v) { 238 mSelectedContainer.setVisibility(VISIBLE); 239 int centerX = v.getLeft() + v.getWidth() / 2; 240 int centerY = v.getTop() + v.getHeight() / 2; 241 Animator reveal = ViewAnimationUtils.createCircularReveal( 242 mSelectedContainer, 243 centerX, 244 centerY, 245 0, 246 Math.max(centerX, mSelectedContainer.getWidth() - centerX) 247 + Math.max(centerY, mSelectedContainer.getHeight() - centerY)); 248 reveal.start(); 249 250 animateHintText(mSelectedLabel, v, reveal); 251 animateHintText(mLaunchHint, v, reveal); 252 253 mSelectedLabel.setText(((Button) v).getText()); 254 mSelectedContainer.setTag(R.id.tag_intent, v.getTag(R.id.tag_intent)); 255 mLastRevealed = v; 256 postDelayed(mHideRunnable, HIDE_DELAY); 257 postDelayed(mRippleRunnable, RIPPLE_PAUSE / 2); 258 259 // Transfer focus from the originally clicked button to the expanded button. 260 mSelectedContainer.requestFocus(); 261 } 262 animateHintText(View selectedView, View v, Animator reveal)263 private void animateHintText(View selectedView, View v, Animator reveal) { 264 selectedView.setTranslationX( 265 (v.getLeft() + v.getWidth() / 2 - mSelectedContainer.getWidth() / 2) / 5); 266 selectedView.animate() 267 .setDuration(reveal.getDuration() / 3) 268 .setStartDelay(reveal.getDuration() / 5) 269 .translationX(0) 270 .setInterpolator(mFastOutLinearInInterpolator) 271 .start(); 272 } 273 hideTheButton()274 private void hideTheButton() { 275 if (mHiding || mSelectedContainer.getVisibility() != VISIBLE) { 276 return; 277 } 278 279 mHiding = true; 280 281 removeCallbacks(mHideRunnable); 282 283 View v = mLastRevealed; 284 int centerX = v.getLeft() + v.getWidth() / 2; 285 int centerY = v.getTop() + v.getHeight() / 2; 286 Animator reveal = ViewAnimationUtils.createCircularReveal( 287 mSelectedContainer, 288 centerX, 289 centerY, 290 Math.max(centerX, mSelectedContainer.getWidth() - centerX) 291 + Math.max(centerY, mSelectedContainer.getHeight() - centerY), 292 0); 293 reveal.addListener(new AnimatorListenerAdapter() { 294 @Override 295 public void onAnimationEnd(Animator animation) { 296 mSelectedContainer.setVisibility(INVISIBLE); 297 removeCallbacks(mRippleRunnable); 298 mHiding = false; 299 } 300 }); 301 reveal.start(); 302 303 // Transfer focus back to the originally clicked button. 304 if (mSelectedContainer.isFocused()) { 305 v.requestFocus(); 306 } 307 } 308 startRipple()309 private void startRipple() { 310 final View ripple = mRippleView; 311 ripple.animate().cancel(); 312 ripple.setVisibility(VISIBLE); 313 Animator reveal = ViewAnimationUtils.createCircularReveal( 314 ripple, 315 ripple.getLeft() + ripple.getWidth() / 2, 316 ripple.getTop() + ripple.getHeight() / 2, 317 0, 318 ripple.getWidth() / 2); 319 reveal.setDuration(RIPPLE_DURATION); 320 reveal.start(); 321 322 ripple.setAlpha(0); 323 ripple.animate().alpha(1).setDuration(RIPPLE_DURATION / 2) 324 .withEndAction(new Runnable() { 325 @Override 326 public void run() { 327 ripple.animate().alpha(0).setDuration(RIPPLE_DURATION / 2) 328 .withEndAction(new Runnable() { 329 @Override 330 public void run() { 331 ripple.setVisibility(INVISIBLE); 332 postDelayed(mRippleRunnable, RIPPLE_PAUSE); 333 } 334 }).start(); 335 } 336 }).start(); 337 } 338 339 private final Runnable mHideRunnable = new Runnable() { 340 @Override 341 public void run() { 342 if (!isAttachedToWindow()) return; 343 hideTheButton(); 344 } 345 }; 346 347 private final Runnable mRippleRunnable = new Runnable() { 348 @Override 349 public void run() { 350 if (!isAttachedToWindow()) return; 351 startRipple(); 352 } 353 }; 354 355 356 } 357