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.Layout; 32 import android.text.TextUtils; 33 import android.util.AttributeSet; 34 import android.util.TypedValue; 35 import android.view.Gravity; 36 import android.view.MotionEvent; 37 import android.view.View; 38 import android.view.ViewAnimationUtils; 39 import android.view.ViewGroup; 40 import android.view.accessibility.AccessibilityManager; 41 import android.view.animation.AnimationUtils; 42 import android.view.animation.Interpolator; 43 import android.widget.Button; 44 import android.widget.FrameLayout; 45 import android.widget.TextView; 46 47 import java.util.List; 48 49 public class EmergencyActionGroup extends FrameLayout implements View.OnClickListener { 50 51 private static final long HIDE_DELAY = 3000; 52 private static final int RIPPLE_DURATION = 600; 53 private static final long RIPPLE_PAUSE = 1000; 54 55 private final Interpolator mFastOutLinearInInterpolator; 56 57 private ViewGroup mSelectedContainer; 58 private TextView mSelectedLabel; 59 private View mRippleView; 60 private View mLaunchHint; 61 62 private View mLastRevealed; 63 64 private MotionEvent mPendingTouchEvent; 65 66 private boolean mHiding; 67 EmergencyActionGroup(Context context, @Nullable AttributeSet attrs)68 public EmergencyActionGroup(Context context, @Nullable AttributeSet attrs) { 69 super(context, attrs); 70 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context, 71 android.R.interpolator.fast_out_linear_in); 72 } 73 74 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)75 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 76 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 77 } 78 79 @Override onFinishInflate()80 protected void onFinishInflate() { 81 super.onFinishInflate(); 82 83 mSelectedContainer = (ViewGroup) findViewById(R.id.selected_container); 84 mSelectedContainer.setOnClickListener(this); 85 mSelectedLabel = (TextView) findViewById(R.id.selected_label); 86 mSelectedLabel.addOnLayoutChangeListener(mLayoutChangeListener); 87 mRippleView = findViewById(R.id.ripple_view); 88 mLaunchHint = findViewById(R.id.launch_hint); 89 mLaunchHint.addOnLayoutChangeListener(mLayoutChangeListener); 90 } 91 92 @Override onWindowVisibilityChanged(int visibility)93 protected void onWindowVisibilityChanged(int visibility) { 94 super.onWindowVisibilityChanged(visibility); 95 if (visibility == View.VISIBLE) { 96 setupAssistActions(); 97 } 98 } 99 100 /** 101 * Called by the activity before a touch event is dispatched to the view hierarchy. 102 */ onPreTouchEvent(MotionEvent event)103 public void onPreTouchEvent(MotionEvent event) { 104 mPendingTouchEvent = event; 105 } 106 107 @Override dispatchTouchEvent(MotionEvent event)108 public boolean dispatchTouchEvent(MotionEvent event) { 109 boolean handled = super.dispatchTouchEvent(event); 110 if (mPendingTouchEvent == event && handled) { 111 mPendingTouchEvent = null; 112 } 113 return handled; 114 } 115 116 /** 117 * Called by the activity after a touch event is dispatched to the view hierarchy. 118 */ onPostTouchEvent(MotionEvent event)119 public void onPostTouchEvent(MotionEvent event) { 120 // Hide the confirmation button if a touch event was delivered to the activity but not to 121 // this view. 122 if (mPendingTouchEvent != null) { 123 hideTheButton(); 124 } 125 mPendingTouchEvent = null; 126 } 127 128 129 setupAssistActions()130 private void setupAssistActions() { 131 int[] buttonIds = new int[] {R.id.action1, R.id.action2, R.id.action3}; 132 133 List<ResolveInfo> infos; 134 135 if (TelephonyManager.EMERGENCY_ASSISTANCE_ENABLED) { 136 infos = resolveAssistPackageAndQueryActivites(); 137 } else { 138 infos = null; 139 } 140 141 for (int i = 0; i < 3; i++) { 142 Button button = (Button) findViewById(buttonIds[i]); 143 boolean visible = false; 144 145 button.setOnClickListener(this); 146 147 if (infos != null && infos.size() > i && infos.get(i) != null) { 148 ResolveInfo info = infos.get(i); 149 ComponentName name = getComponentName(info); 150 151 button.setTag(R.id.tag_intent, 152 new Intent(TelephonyManager.ACTION_EMERGENCY_ASSISTANCE) 153 .setComponent(name)); 154 button.setText(info.loadLabel(getContext().getPackageManager())); 155 visible = true; 156 } 157 158 button.setVisibility(visible ? View.VISIBLE : View.GONE); 159 } 160 } 161 resolveAssistPackageAndQueryActivites()162 private List<ResolveInfo> resolveAssistPackageAndQueryActivites() { 163 List<ResolveInfo> infos = queryAssistActivities(); 164 165 if (infos == null || infos.isEmpty()) { 166 PackageManager packageManager = getContext().getPackageManager(); 167 Intent queryIntent = new Intent(TelephonyManager.ACTION_EMERGENCY_ASSISTANCE); 168 infos = packageManager.queryIntentActivities(queryIntent, 0); 169 170 PackageInfo bestMatch = null; 171 for (int i = 0; i < infos.size(); i++) { 172 if (infos.get(i).activityInfo == null) continue; 173 String packageName = infos.get(i).activityInfo.packageName; 174 PackageInfo packageInfo; 175 try { 176 packageInfo = packageManager.getPackageInfo(packageName, 0); 177 } catch (PackageManager.NameNotFoundException e) { 178 continue; 179 } 180 // Get earliest installed system app. 181 if (isSystemApp(packageInfo) && (bestMatch == null || 182 bestMatch.firstInstallTime > packageInfo.firstInstallTime)) { 183 bestMatch = packageInfo; 184 } 185 } 186 187 if (bestMatch != null) { 188 Settings.Secure.putString(getContext().getContentResolver(), 189 Settings.Secure.EMERGENCY_ASSISTANCE_APPLICATION, 190 bestMatch.packageName); 191 return queryAssistActivities(); 192 } else { 193 return null; 194 } 195 } else { 196 return infos; 197 } 198 } 199 queryAssistActivities()200 private List<ResolveInfo> queryAssistActivities() { 201 String assistPackage = Settings.Secure.getString( 202 getContext().getContentResolver(), 203 Settings.Secure.EMERGENCY_ASSISTANCE_APPLICATION); 204 List<ResolveInfo> infos = null; 205 206 if (!TextUtils.isEmpty(assistPackage)) { 207 Intent queryIntent = new Intent(TelephonyManager.ACTION_EMERGENCY_ASSISTANCE) 208 .setPackage(assistPackage); 209 infos = getContext().getPackageManager().queryIntentActivities(queryIntent, 0); 210 } 211 return infos; 212 } 213 isSystemApp(PackageInfo info)214 private boolean isSystemApp(PackageInfo info) { 215 return info.applicationInfo != null 216 && (info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 217 } 218 getComponentName(ResolveInfo resolveInfo)219 private ComponentName getComponentName(ResolveInfo resolveInfo) { 220 if (resolveInfo == null || resolveInfo.activityInfo == null) return null; 221 return new ComponentName(resolveInfo.activityInfo.packageName, 222 resolveInfo.activityInfo.name); 223 } 224 225 @Override onClick(View v)226 public void onClick(View v) { 227 Intent intent = (Intent) v.getTag(R.id.tag_intent); 228 229 switch (v.getId()) { 230 case R.id.action1: 231 case R.id.action2: 232 case R.id.action3: 233 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { 234 getContext().startActivity(intent); 235 } else { 236 revealTheButton(v); 237 } 238 break; 239 case R.id.selected_container: 240 if (!mHiding) { 241 getContext().startActivity(intent); 242 } 243 break; 244 } 245 } 246 revealTheButton(View v)247 private void revealTheButton(View v) { 248 CharSequence buttonText = ((Button) v).getText(); 249 mSelectedLabel.setText(buttonText); 250 mSelectedLabel.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM); 251 252 // In order to trigger OnLayoutChangeListener for reset default minimum font size. 253 mSelectedLabel.requestLayout(); 254 mLaunchHint.requestLayout(); 255 256 mSelectedContainer.setVisibility(VISIBLE); 257 int centerX = v.getLeft() + v.getWidth() / 2; 258 int centerY = v.getTop() + v.getHeight() / 2; 259 Animator reveal = ViewAnimationUtils.createCircularReveal( 260 mSelectedContainer, 261 centerX, 262 centerY, 263 0, 264 Math.max(centerX, mSelectedContainer.getWidth() - centerX) 265 + Math.max(centerY, mSelectedContainer.getHeight() - centerY)); 266 reveal.start(); 267 268 animateHintText(mSelectedLabel, v, reveal); 269 animateHintText(mLaunchHint, v, reveal); 270 271 mSelectedContainer.setTag(R.id.tag_intent, v.getTag(R.id.tag_intent)); 272 mLastRevealed = v; 273 postDelayed(mHideRunnable, HIDE_DELAY); 274 postDelayed(mRippleRunnable, RIPPLE_PAUSE / 2); 275 276 // Transfer focus from the originally clicked button to the expanded button. 277 mSelectedContainer.requestFocus(); 278 } 279 280 281 private final OnLayoutChangeListener mLayoutChangeListener = new OnLayoutChangeListener() { 282 @Override 283 public void onLayoutChange(View v, int left, int top, int right, int bottom, 284 int oldLeft, 285 int oldTop, int oldRight, int oldBottom) { 286 decreaseAutoSizeMinTextSize(v); 287 } 288 }; 289 290 /** 291 * Prevent some localization string will be truncated if there is low resolution screen 292 * or font size and display size of setting is largest. 293 */ decreaseAutoSizeMinTextSize(View selectedView)294 private void decreaseAutoSizeMinTextSize(View selectedView) { 295 if (selectedView != null) { 296 if (selectedView instanceof TextView) { 297 TextView textView = (TextView) selectedView; 298 textView.setEllipsize(TextUtils.TruncateAt.END); 299 300 // The textView layout will be null due to it's property is hiding when 301 // initialization. 302 Layout layout = textView.getLayout(); 303 if (layout != null) { 304 if (layout.getEllipsisCount(textView.getMaxLines() - 1) > 0) { 305 textView.setAutoSizeTextTypeUniformWithConfiguration( 306 8, 307 textView.getAutoSizeMaxTextSize(), 308 textView.getAutoSizeStepGranularity(), 309 TypedValue.COMPLEX_UNIT_SP); 310 textView.setGravity(Gravity.CENTER); 311 } 312 } 313 } 314 } 315 } 316 animateHintText(View selectedView, View v, Animator reveal)317 private void animateHintText(View selectedView, View v, Animator reveal) { 318 selectedView.setTranslationX( 319 (v.getLeft() + v.getWidth() / 2 - mSelectedContainer.getWidth() / 2) / 5); 320 selectedView.animate() 321 .setDuration(reveal.getDuration() / 3) 322 .setStartDelay(reveal.getDuration() / 5) 323 .translationX(0) 324 .setInterpolator(mFastOutLinearInInterpolator) 325 .start(); 326 } 327 hideTheButton()328 private void hideTheButton() { 329 if (mHiding || mSelectedContainer.getVisibility() != VISIBLE) { 330 return; 331 } 332 333 mHiding = true; 334 335 removeCallbacks(mHideRunnable); 336 337 View v = mLastRevealed; 338 int centerX = v.getLeft() + v.getWidth() / 2; 339 int centerY = v.getTop() + v.getHeight() / 2; 340 Animator reveal = ViewAnimationUtils.createCircularReveal( 341 mSelectedContainer, 342 centerX, 343 centerY, 344 Math.max(centerX, mSelectedContainer.getWidth() - centerX) 345 + Math.max(centerY, mSelectedContainer.getHeight() - centerY), 346 0); 347 reveal.addListener(new AnimatorListenerAdapter() { 348 @Override 349 public void onAnimationEnd(Animator animation) { 350 mSelectedContainer.setVisibility(INVISIBLE); 351 removeCallbacks(mRippleRunnable); 352 mHiding = false; 353 } 354 }); 355 reveal.start(); 356 357 // Transfer focus back to the originally clicked button. 358 if (mSelectedContainer.isFocused()) { 359 v.requestFocus(); 360 } 361 } 362 startRipple()363 private void startRipple() { 364 final View ripple = mRippleView; 365 ripple.animate().cancel(); 366 ripple.setVisibility(VISIBLE); 367 Animator reveal = ViewAnimationUtils.createCircularReveal( 368 ripple, 369 ripple.getLeft() + ripple.getWidth() / 2, 370 ripple.getTop() + ripple.getHeight() / 2, 371 0, 372 ripple.getWidth() / 2); 373 reveal.setDuration(RIPPLE_DURATION); 374 reveal.start(); 375 376 ripple.setAlpha(0); 377 ripple.animate().alpha(1).setDuration(RIPPLE_DURATION / 2) 378 .withEndAction(new Runnable() { 379 @Override 380 public void run() { 381 ripple.animate().alpha(0).setDuration(RIPPLE_DURATION / 2) 382 .withEndAction(new Runnable() { 383 @Override 384 public void run() { 385 ripple.setVisibility(INVISIBLE); 386 postDelayed(mRippleRunnable, RIPPLE_PAUSE); 387 } 388 }).start(); 389 } 390 }).start(); 391 } 392 393 private final Runnable mHideRunnable = new Runnable() { 394 @Override 395 public void run() { 396 if (!isAttachedToWindow()) return; 397 hideTheButton(); 398 } 399 }; 400 401 private final Runnable mRippleRunnable = new Runnable() { 402 @Override 403 public void run() { 404 if (!isAttachedToWindow()) return; 405 startRipple(); 406 } 407 }; 408 409 410 } 411