| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 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; | 
| Adrian Roos | 1c4b47f | 2015-04-13 14:53:32 -0700 | [diff] [blame] | 33 | import android.view.MotionEvent; | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 34 | import android.view.View; | 
 | 35 | import android.view.ViewAnimationUtils; | 
 | 36 | import android.view.ViewGroup; | 
| Adrian Roos | ee55c73 | 2015-04-13 14:53:53 -0700 | [diff] [blame] | 37 | import android.view.accessibility.AccessibilityManager; | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 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 |  | 
| Adrian Roos | 1c4b47f | 2015-04-13 14:53:32 -0700 | [diff] [blame] | 61 |     private MotionEvent mPendingTouchEvent; | 
 | 62 |  | 
 | 63 |     private boolean mHiding; | 
 | 64 |  | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 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(); | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 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 |  | 
| mariagpuyol | d0056c0 | 2016-03-29 12:12:40 -0700 | [diff] [blame] | 87 |     @Override | 
 | 88 |     protected void onWindowVisibilityChanged(int visibility) { | 
 | 89 |         super.onWindowVisibilityChanged(visibility); | 
 | 90 |         if (visibility == View.VISIBLE) { | 
 | 91 |             setupAssistActions(); | 
 | 92 |         } | 
 | 93 |     } | 
 | 94 |  | 
| Adrian Roos | 1c4b47f | 2015-04-13 14:53:32 -0700 | [diff] [blame] | 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 |  | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 125 |     private void setupAssistActions() { | 
 | 126 |         int[] buttonIds = new int[] {R.id.action1, R.id.action2, R.id.action3}; | 
 | 127 |  | 
| Adrian Roos | 9946f8b | 2015-05-27 14:37:58 -0700 | [diff] [blame] | 128 |         List<ResolveInfo> infos; | 
 | 129 |  | 
 | 130 |         if (TelephonyManager.EMERGENCY_ASSISTANCE_ENABLED) { | 
 | 131 |             infos = resolveAssistPackageAndQueryActivites(); | 
 | 132 |         } else { | 
 | 133 |             infos = null; | 
 | 134 |         } | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 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 |                 } | 
| Akshay Kannan | 5d927b4 | 2016-02-09 13:51:17 -0800 | [diff] [blame] | 175 |                 // Get earliest installed system app. | 
 | 176 |                 if (isSystemApp(packageInfo) && (bestMatch == null || | 
 | 177 |                         bestMatch.firstInstallTime > packageInfo.firstInstallTime)) { | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 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) { | 
| Adrian Roos | 1c4b47f | 2015-04-13 14:53:32 -0700 | [diff] [blame] | 222 |         Intent intent = (Intent) v.getTag(R.id.tag_intent); | 
 | 223 |  | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 224 |         switch (v.getId()) { | 
 | 225 |             case R.id.action1: | 
 | 226 |             case R.id.action2: | 
 | 227 |             case R.id.action3: | 
| Adrian Roos | ee55c73 | 2015-04-13 14:53:53 -0700 | [diff] [blame] | 228 |                 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { | 
 | 229 |                     getContext().startActivity(intent); | 
 | 230 |                 } else { | 
 | 231 |                     revealTheButton(v); | 
 | 232 |                 } | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 233 |                 break; | 
 | 234 |             case R.id.selected_container: | 
| Adrian Roos | 1c4b47f | 2015-04-13 14:53:32 -0700 | [diff] [blame] | 235 |                 if (!mHiding) { | 
 | 236 |                     getContext().startActivity(intent); | 
 | 237 |                 } | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 238 |                 break; | 
 | 239 |         } | 
 | 240 |     } | 
 | 241 |  | 
 | 242 |     private void revealTheButton(View v) { | 
| Hall Liu | 21a3779 | 2017-07-26 15:49:27 -0700 | [diff] [blame] | 243 |         CharSequence buttonText = ((Button) v).getText(); | 
 | 244 |         mSelectedLabel.setText(buttonText); | 
 | 245 |         mSelectedLabel.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM); | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 246 |         mSelectedContainer.setVisibility(VISIBLE); | 
 | 247 |         int centerX = v.getLeft() + v.getWidth() / 2; | 
 | 248 |         int centerY = v.getTop() + v.getHeight() / 2; | 
 | 249 |         Animator reveal = ViewAnimationUtils.createCircularReveal( | 
 | 250 |                 mSelectedContainer, | 
 | 251 |                 centerX, | 
 | 252 |                 centerY, | 
 | 253 |                 0, | 
 | 254 |                 Math.max(centerX, mSelectedContainer.getWidth() - centerX) | 
 | 255 |                         + Math.max(centerY, mSelectedContainer.getHeight() - centerY)); | 
 | 256 |         reveal.start(); | 
 | 257 |  | 
 | 258 |         animateHintText(mSelectedLabel, v, reveal); | 
 | 259 |         animateHintText(mLaunchHint, v, reveal); | 
 | 260 |  | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 261 |         mSelectedContainer.setTag(R.id.tag_intent, v.getTag(R.id.tag_intent)); | 
 | 262 |         mLastRevealed = v; | 
| Adrian Roos | 1c4b47f | 2015-04-13 14:53:32 -0700 | [diff] [blame] | 263 |         postDelayed(mHideRunnable, HIDE_DELAY); | 
 | 264 |         postDelayed(mRippleRunnable, RIPPLE_PAUSE / 2); | 
 | 265 |  | 
 | 266 |         // Transfer focus from the originally clicked button to the expanded button. | 
 | 267 |         mSelectedContainer.requestFocus(); | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 268 |     } | 
 | 269 |  | 
 | 270 |     private void animateHintText(View selectedView, View v, Animator reveal) { | 
 | 271 |         selectedView.setTranslationX( | 
 | 272 |                 (v.getLeft() + v.getWidth() / 2 - mSelectedContainer.getWidth() / 2) / 5); | 
 | 273 |         selectedView.animate() | 
 | 274 |                 .setDuration(reveal.getDuration() / 3) | 
 | 275 |                 .setStartDelay(reveal.getDuration() / 5) | 
 | 276 |                 .translationX(0) | 
 | 277 |                 .setInterpolator(mFastOutLinearInInterpolator) | 
 | 278 |                 .start(); | 
 | 279 |     } | 
 | 280 |  | 
 | 281 |     private void hideTheButton() { | 
| Adrian Roos | 1c4b47f | 2015-04-13 14:53:32 -0700 | [diff] [blame] | 282 |         if (mHiding || mSelectedContainer.getVisibility() != VISIBLE) { | 
 | 283 |             return; | 
 | 284 |         } | 
 | 285 |  | 
 | 286 |         mHiding = true; | 
 | 287 |  | 
 | 288 |         removeCallbacks(mHideRunnable); | 
 | 289 |  | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 290 |         View v = mLastRevealed; | 
 | 291 |         int centerX = v.getLeft() + v.getWidth() / 2; | 
 | 292 |         int centerY = v.getTop() + v.getHeight() / 2; | 
 | 293 |         Animator reveal = ViewAnimationUtils.createCircularReveal( | 
 | 294 |                 mSelectedContainer, | 
 | 295 |                 centerX, | 
 | 296 |                 centerY, | 
 | 297 |                 Math.max(centerX, mSelectedContainer.getWidth() - centerX) | 
 | 298 |                         + Math.max(centerY, mSelectedContainer.getHeight() - centerY), | 
 | 299 |                 0); | 
 | 300 |         reveal.addListener(new AnimatorListenerAdapter() { | 
 | 301 |             @Override | 
 | 302 |             public void onAnimationEnd(Animator animation) { | 
 | 303 |                 mSelectedContainer.setVisibility(INVISIBLE); | 
| Adrian Roos | 1c4b47f | 2015-04-13 14:53:32 -0700 | [diff] [blame] | 304 |                 removeCallbacks(mRippleRunnable); | 
 | 305 |                 mHiding = false; | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 306 |             } | 
 | 307 |         }); | 
 | 308 |         reveal.start(); | 
| Adrian Roos | 1c4b47f | 2015-04-13 14:53:32 -0700 | [diff] [blame] | 309 |  | 
 | 310 |         // Transfer focus back to the originally clicked button. | 
 | 311 |         if (mSelectedContainer.isFocused()) { | 
 | 312 |             v.requestFocus(); | 
 | 313 |         } | 
| Adrian Roos | 4e664b6 | 2015-03-25 15:09:46 -0700 | [diff] [blame] | 314 |     } | 
 | 315 |  | 
 | 316 |     private void startRipple() { | 
 | 317 |         final View ripple = mRippleView; | 
 | 318 |         ripple.animate().cancel(); | 
 | 319 |         ripple.setVisibility(VISIBLE); | 
 | 320 |         Animator reveal = ViewAnimationUtils.createCircularReveal( | 
 | 321 |                 ripple, | 
 | 322 |                 ripple.getLeft() + ripple.getWidth() / 2, | 
 | 323 |                 ripple.getTop() + ripple.getHeight() / 2, | 
 | 324 |                 0, | 
 | 325 |                 ripple.getWidth() / 2); | 
 | 326 |         reveal.setDuration(RIPPLE_DURATION); | 
 | 327 |         reveal.start(); | 
 | 328 |  | 
 | 329 |         ripple.setAlpha(0); | 
 | 330 |         ripple.animate().alpha(1).setDuration(RIPPLE_DURATION / 2) | 
 | 331 |                 .withEndAction(new Runnable() { | 
 | 332 |             @Override | 
 | 333 |             public void run() { | 
 | 334 |                 ripple.animate().alpha(0).setDuration(RIPPLE_DURATION / 2) | 
 | 335 |                         .withEndAction(new Runnable() { | 
 | 336 |                             @Override | 
 | 337 |                             public void run() { | 
 | 338 |                                 ripple.setVisibility(INVISIBLE); | 
 | 339 |                                 postDelayed(mRippleRunnable, RIPPLE_PAUSE); | 
 | 340 |                             } | 
 | 341 |                         }).start(); | 
 | 342 |             } | 
 | 343 |         }).start(); | 
 | 344 |     } | 
 | 345 |  | 
 | 346 |     private final Runnable mHideRunnable = new Runnable() { | 
 | 347 |         @Override | 
 | 348 |         public void run() { | 
 | 349 |             if (!isAttachedToWindow()) return; | 
 | 350 |             hideTheButton(); | 
 | 351 |         } | 
 | 352 |     }; | 
 | 353 |  | 
 | 354 |     private final Runnable mRippleRunnable = new Runnable() { | 
 | 355 |         @Override | 
 | 356 |         public void run() { | 
 | 357 |             if (!isAttachedToWindow()) return; | 
 | 358 |             startRipple(); | 
 | 359 |         } | 
 | 360 |     }; | 
 | 361 |  | 
 | 362 |  | 
 | 363 | } |