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